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 BehaviorSubject when it has a current value), plain event handlers push into it with .next(...), and components read it with a hook. The 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 living 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, and — living outside the component —
// keeps its 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 — and 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 — synchronous updates.
  const name = useSyncObservable(
    name$,
    name$.getValue(),
  )
  // Age only feeds rendering — 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