Skip to Content
ExamplesFirst steps

First steps

This page mirrors the RxJS overview  — the same progression of examples, but inside React components. Where the RxJS guide contrasts plain JavaScript with observables, we contrast React + RxJS wired by hand with react-rx. If observables themselves are new to you, skim that guide first; react-rx assumes the basics.

Counting clicks

Normally, bridging a stream into React means owning the whole lifecycle yourself: a Subject for the events, a useEffect to subscribe, a mirrored useState to hold the latest value, and an unsubscribe on unmount. Every copy of this is a chance for a leak or a stale closure:

import {useEffect, useMemo, useState} from 'react'
import {scan, Subject} from 'rxjs'

// Without react-rx you own the whole bridge: the subscription, a mirrored
// piece of useState, and the teardown.
export default function App() {
  const [clicks$] = useState(
    () => new Subject<void>(),
  )
  const count$ = useMemo(
    () =>
      clicks$.pipe(scan((count) => count + 1, 0)),
    [clicks$],
  )

  const [count, setCount] = useState(0)
  useEffect(() => {
    const subscription =
      count$.subscribe(setCount)
    return () => subscription.unsubscribe()
  }, [count$])

  return (
    <button
      type="button"
      onClick={() => clicks$.next()}
    >
      Clicked {count} times
    </button>
  )
}

Open on CodeSandboxOpen Sandbox

Using react-rx, the component just reads the stream. The count state lives in the pipe — scan works like reduce for arrays — and the hook owns subscription, initial value, and teardown:

import {useMemo, useState} from 'react'
import {useObservable} from 'react-rx'
import {scan, Subject} from 'rxjs'

// With react-rx the hook owns the subscription, the initial value and the
// teardown. The count state lives in the stream (scan), not in a variable.
export default function App() {
  const [clicks$] = useState(
    () => new Subject<void>(),
  )
  const count$ = useMemo(
    () =>
      clicks$.pipe(scan((count) => count + 1, 0)),
    [clicks$],
  )
  const count = useObservable(count$, 0)

  return (
    <button
      type="button"
      onClick={() => clicks$.next()}
    >
      Clicked {count} times
    </button>
  )
}

Open on CodeSandboxOpen Sandbox

The bridge boilerplate is gone, and something subtler improved too: the state can no longer be mutated from anywhere else. The only way to change the count is to emit a click.

Flow

RxJS has a whole range of operators  that control how events flow through your streams. In vanilla React, “count at most one click per second” means refs, timestamps, and an easy-to-botch comparison. In a stream it is one operator:

import {useMemo, useState} from 'react'
import {useObservable} from 'react-rx'
import {scan, Subject, throttleTime} from 'rxjs'

// Controlling the flow of events is one operator: click as fast as you like —
// at most one click per second makes it into the count.
export default function App() {
  const [clicks$] = useState(
    () => new Subject<void>(),
  )
  const count$ = useMemo(
    () =>
      clicks$.pipe(
        throttleTime(1000),
        scan((count) => count + 1, 0),
      ),
    [clicks$],
  )
  const count = useObservable(count$, 0)

  return (
    <>
      <button
        type="button"
        onClick={() => clicks$.next()}
      >
        Click as fast as you can
      </button>
      <p>
        Counted {count} (at most one per second)
      </p>
    </>
  )
}

Open on CodeSandboxOpen Sandbox

Values

You can transform the values passing through. Here every click contributes its pointer’s x position to a running sum — map plucks the coordinate, scan accumulates it, and the component renders whatever comes out:

import {useMemo, useState} from 'react'
import {useObservable} from 'react-rx'
import {
  map,
  scan,
  Subject,
  throttleTime,
} from 'rxjs'

// Transform the values flowing through: push the whole click event, pluck the
// pointer's x position in the pipe, and sum the positions with scan.
export default function App() {
  const [clicks$] = useState(
    () => new Subject<{clientX: number}>(),
  )
  const total$ = useMemo(
    () =>
      clicks$.pipe(
        throttleTime(1000),
        map((event) => event.clientX),
        scan((sum, clientX) => sum + clientX, 0),
      ),
    [clicks$],
  )
  const total = useObservable(total$, 0)

  return (
    <>
      <button
        type="button"
        onClick={(event) => clicks$.next(event)}
      >
        Click me (anywhere on the button)
      </button>
      <p>Sum of x positions: {total}</p>
    </>
  )
}

Open on CodeSandboxOpen Sandbox

Where to next

Basic state rebuilds the useState docs examples on streams, and Timers & time ago shows where streams beat hooks hardest. For choosing between the hooks you just saw, read which hook should I use?

Last updated on