Skip to Content
ExamplesBasic state

Basic state

The useState reference opens with four basic state shapes: a counter, a text field, a checkbox, and a form. First steps covered the counter. Here are the other three, as streams.

The pattern is always the same:

  • State lives in a Subject (or a BehaviorSubject when it has a current value).
  • Plain event handlers push into it with .next(...).
  • Components read it with a hook.

One split to remember: controlled inputs read useSyncObservable. The caret and IME need synchronous updates. Everything else defaults to useObservable.

Text field (string)

import {useState} from 'react'
import {useSyncObservable} from 'react-rx'
import {Subject} from 'rxjs'

export default function App() {
  const [text$] = useState(
    () => new Subject<string>(),
  )
  // Controlled inputs read useSyncObservable. The value must update
  // synchronously to keep the caret and IME composition intact.
  const text = useSyncObservable(text$, 'hello')

  return (
    <>
      <input
        value={text}
        onChange={(e) =>
          text$.next(e.currentTarget.value)
        }
      />
      <p>You typed: {text}</p>
      <button
        type="button"
        onClick={() => text$.next('hello')}
      >
        Reset
      </button>
    </>
  )
}

Open on CodeSandboxOpen Sandbox

Checkbox (boolean)

This one uses a module-scoped BehaviorSubject. It holds a current value and emits it synchronously, so the first render already has the real state. And because it lives outside the component, it survives remounts and can be shared or composed with other streams.

Compare with the text field above, where useState(() => new Subject()) keeps the state per component instance.

import {useSyncObservable} from 'react-rx'
import {BehaviorSubject} from 'rxjs'

// A module-scoped BehaviorSubject: it holds a current value, emits it
// synchronously to new subscribers. Living outside the component, it
// keeps state across remounts and can be shared or composed anywhere.
const liked$ = new BehaviorSubject(true)

export default function App() {
  // The synchronous emission means the first render already has the real
  // value. The initialValue argument is only a fallback for TypeScript here.
  const liked = useSyncObservable(liked$, true)

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={liked}
          onChange={(e) =>
            liked$.next(e.currentTarget.checked)
          }
        />
        I liked this
      </label>
      <p>
        You {liked ? 'liked' : 'did not like'}{' '}
        this.
      </p>
    </>
  )
}

Open on CodeSandboxOpen Sandbox

Form (two variables)

Two independent pieces of state, just like two useState calls. Also a preview of the hook split: the name feeds a controlled input (useSyncObservable), while the age only feeds rendering (useObservable).

import {useState} from 'react'
import {
  useObservable,
  useSyncObservable,
} from 'react-rx'
import {BehaviorSubject} from 'rxjs'

export default function App() {
  const [name$] = useState(
    () => new BehaviorSubject('Taylor'),
  )
  const [age$] = useState(
    () => new BehaviorSubject(42),
  )

  // The text input is controlled, so it needs synchronous updates.
  const name = useSyncObservable(
    name$,
    name$.getValue(),
  )
  // Age only feeds rendering, so the deferred default is fine.
  const age = useObservable(age$, age$.getValue())

  return (
    <>
      <input
        value={name}
        onChange={(e) =>
          name$.next(e.currentTarget.value)
        }
      />
      <button
        type="button"
        onClick={() =>
          age$.next(age$.getValue() + 1)
        }
      >
        Increment age
      </button>
      <p>
        Hello, {name}. You are {age}.
      </p>
    </>
  )
}

Open on CodeSandboxOpen Sandbox

Where to next

So far streams have only replaced useState. The payoff starts when state involves time. Continue to Timers & time ago.

Last updated on