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, useState} from 'react'
import {scan, Subject} from 'rxjs'

// Clicks push into a Subject; the count lives in the stream — scan works
// like reduce for arrays.
const clicks$ = new Subject<void>()
const count$ = clicks$.pipe(
  scan((count) => count + 1, 0),
)

// Without react-rx you own the whole bridge: the subscription, a mirrored
// piece of useState, and the teardown.
export default function App() {
  const [count, setCount] = useState(0)

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

  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 {useObservable} from 'react-rx'
import {scan, Subject} from 'rxjs'

// The same two streams…
const clicks$ = new Subject<void>()
const count$ = clicks$.pipe(
  scan((count) => count + 1, 0),
)

// …and the whole bridge is one hook: subscription, initial value and
// teardown are owned by useObservable.
export default function App() {
  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.

(The streams live at module scope here so the components stay minimal — one shared instance per sandbox. For per-component streams, create them with useState(() => new Subject()) instead; Basic state shows both styles.)

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 {useObservable} from 'react-rx'
import {scan, Subject, throttleTime} from 'rxjs'

const clicks$ = new Subject<void>()
// 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.
const count$ = clicks$.pipe(
  throttleTime(1000),
  scan((count) => count + 1, 0),
)

export default function App() {
  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 {useObservable} from 'react-rx'
import {
  map,
  scan,
  Subject,
  throttleTime,
} from 'rxjs'

const clicks$ = new Subject<{clientX: number}>()
// 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.
const total$ = clicks$.pipe(
  throttleTime(1000),
  map((event) => event.clientX),
  scan((sum, clientX) => sum + clientX, 0),
)

export default function App() {
  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