Skip to Content
ExamplesSuspense data fetching

Suspense data fetching

useObservablePromise returns a promise for React’s use(). The Suspense fallback shows until the first emission; later stream updates do not re-trigger it.

import {Suspense, use, useState} from 'react'
import {useObservablePromise, type ObservablePromise} from 'react-rx'
import {map, type Observable, timer} from 'rxjs'

const LATENCY_MS = 800

const userCache = new Map<string, Observable<{id: string; name: string; bio: string}>>()

/** Stable per id so Suspense retries and remounts share one in-flight request. */
function fetchUser$(id: string) {
  let observable = userCache.get(id)
  if (!observable) {
    observable = timer(LATENCY_MS).pipe(
      map(() => ({
        id,
        name: id === 'alpha' ? 'Ada Lovelace' : 'Grace Hopper',
        bio: `Profile loaded for ${id}`,
      })),
    )
    userCache.set(id, observable)
  }
  return observable
}

const clock$ = timer(0, 1000).pipe(
  map((n) => `Tick ${n} — live updates skip the Suspense fallback`),
)

function Profile({
  promise,
}: {
  promise: ObservablePromise<{id: string; name: string; bio: string}>
}) {
  const user = use(promise)
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.bio}</p>
    </div>
  )
}

function LiveClock() {
  const label = use(useObservablePromise(clock$))
  return <p style={{fontSize: 14, opacity: 0.8}}>{label}</p>
}

export default function SuspenseExample() {
  const [id, setId] = useState('alpha')
  // Create the promise in a parent that does not suspend, so Suspense retries
  // always see the same promise identity (see React's use() caching guidance).
  const promise = useObservablePromise(fetchUser$(id))

  return (
    <div style={{fontFamily: 'system-ui', padding: 16}}>
      <p>
        <button type="button" onClick={() => setId('alpha')}>
          alpha
        </button>{' '}
        <button type="button" onClick={() => setId('beta')}>
          beta
        </button>
      </p>
      <Suspense
        key={id}
        fallback={<p style={{opacity: 0.7}}>Loading {id}</p>}
      >
        <Profile promise={promise} />
      </Suspense>
      <hr style={{margin: '16px 0'}} />
      <Suspense fallback={null}>
        <LiveClock />
      </Suspense>
      <p style={{marginTop: 16, fontSize: 14, opacity: 0.75}}>
        Switching profiles re-suspends (new observable). The clock updates
        continuously without flashing a fallback.
      </p>
    </div>
  )
}

Open on CodeSandboxOpen Sandbox
Last updated on