Skip to content

raf-effect-atom and effect-atom-boundary are on npm. Both are pre-1.0, so a minor release can still break them. Install them

raf-effect-atom

API reference

raf-effect-atom

Render-as-you-fetch for Effect Atom: start atoms from route loaders and stream them into the browser, typed failures intact

import {
  Hydration,
  Loader,
  RequestRegistry,
  useAtomResult,
  useHandleValue,
  HydrationBoundary,
  RegistryProvider,
  WaterfallDetection,
  useWaterfall,
} from "raf-effect-atom";

Handle

Handles: what a loader or server component hands to the component that reads an atom. The loader verbs and useWaterfall return them, and useAtomResult reads one through the atom family that made it, so a component can't start a request from an id it was given.

A handle is plain data, { args, key }, so loader data and server component props carry it as is.

Source

typemodelssince 0.1.0Source

type Source =
  | Atom.Atom<AsyncResult.AsyncResult<any, any>>
  | ((...args: any) => Atom.Atom<AsyncResult.AsyncResult<any, any>>);

What the verbs start and useAtomResult reads: an atom family, a function from arguments to an AsyncResult atom such as an AtomRpc query, or an AsyncResult atom that takes no arguments.

ArgsOf

typeutility typessince 0.1.0Source

type ArgsOf<S> = S extends (...args: infer Args) => unknown ? Args : [];

The arguments a source takes: a family's parameters, or none for an atom.

SuccessOf

typeutility typessince 0.1.0Source

type SuccessOf<S> =
  ResultOf<S> extends AsyncResult.AsyncResult<infer A, infer _E>
    ? A
    : never;

The success type of a source's AsyncResult.

ErrorOf

typeutility typessince 0.1.0Source

type ErrorOf<S> =
  ResultOf<S> extends AsyncResult.AsyncResult<infer _A, infer E>
    ? E
    : never;

The error type of a source's AsyncResult.

HandleKey

typemodelssince 0.1.0Source

type HandleKey<S> = string & {
  /** Type-only: ties the handle to its source. Never present at runtime. */
  readonly [SourceType]?: S;
};

A handle's serialization key, branded with its source. The brand sits on a string, so types that map loader data to what survives serialization, such as React Router's, keep it as is.

Handle

interfacemodelssince 0.1.0Source

interface Handle<S> {
  readonly args: ArgsOf<S>;
  /**
   * `true` when `Loader.prefetchInBrowser` ran on the server and left the
   * atom to the browser: the browser's read starts it.
   */
  readonly inBrowser?: true;
  /** The atom's serialization key; `undefined` for an atom without one. */
  readonly key: HandleKey<S> | undefined;
}

An atom a loader started: its source's arguments, and the atom's serialization key. Its type names its source, so useAtomResult reads it only through a source with the same arguments and success type.

Details

Loader.prefetch and useWaterfall return it, without waiting: the component that reads it suspends until the result settles.

See also

LoadedHandle

interfacemodelssince 0.1.0Source

interface LoadedHandle<S> extends Handle<S> {
  readonly loaded: true;
}

A handle from Loader.ensure or Loader.fetch: they waited for the result, so the component that reads it renders without suspending.

Details

A LoadedHandle fits a Handle prop, and a Handle doesn't fit a LoadedHandle prop. In the loader that ran the verb, Loader.result returns the result it waited for.

Hydration

namespaceSource

Streams a server registry into the browser's, typed failures intact, and hydrates it without requesting anything again.

dehydrate encodes the request registry's serializable atoms. Settled results cross as values; results still loading cross as promises that resolve when they settle, carried by the framework's own streaming. hydrate applies settled values at once and holds each streamed result until its promise resolves, so useAtomResult waits for it instead of starting the atom. hydrated keeps a hydrated value on the atom's first read instead of running its effect again.

Typed failures survive because a serializable AsyncResult atom, such as an AtomRpc query with a serializationKey, encodes its Cause with the procedure's error Schemas: a server-side IssueNotFound decodes as an IssueNotFound in the browser.

import { Hydration } from "raf-effect-atom";

Hydration.hydrated

constcombinatorssince 0.1.0Source

const hydrated: <Self extends Atom.Atom<unknown>>(atom: Self) => Self;

Returns a serializable atom that keeps its hydrated value on its first read, instead of running its effect again.

When to use

The loader verbs and useAtomResult already read through it. Use it when reading a hydrated atom with @effect/atom-react's own hooks.

Details

The copy shares the atom's serialization key, so both read the same registry node. It sets skipInitialRun on the atom's Serializable metadata, and is memoized per atom, so every call returns the same copy. An atom that isn't serializable, or already skips its initial run, is returned as it is.

Gotchas

The copy keeps the hydrated value only for the first read: a refresh, or a change to a dependency, runs the effect as usual. An Initial value never skips the effect, which is why a result the server was still loading crosses as a promise that useAtomResult waits for.

Example

import { useAtomValue } from "@effect/atom-react"
import { Hydration } from "raf-effect-atom"

const result = useAtomValue(Hydration.hydrated(issue(1)))

See also

  • Atom.serializable for the metadata it copies

Hydration.dehydrate

constdehydrationsince 0.1.0Source

const dehydrate: (
  registry: AtomRegistry.AtomRegistry,
) => Array<EffectHydration.DehydratedAtom>;

Encodes a request registry's serializable atoms for the browser.

Details

Hydration.dehydrate with encodeInitialAs: "promise": a settled result crosses as its encoded value, and a result still loading crosses as a promise of its encoded value, which resolves when it settles. The framework's serializer carries the promises (TanStack Router's dehydrated state, React's server component props, React Router's Single Fetch).

Gotchas

Only atoms in the registry when it's called are included, so call it once the loaders have started theirs.

See also

Hydration.hydrate

consthydrationsince 0.1.0Source

const hydrate: (
  registry: AtomRegistry.AtomRegistry,
  state: Iterable<EffectHydration.DehydratedAtom>,
) => void;

Applies dehydrated state to the browser's registry.

When to use

Use outside React, such as in a router's hydrate hook, before the app renders. Inside React, use HydrationBoundary.

Details

Settled values go through Hydration.hydrate. A streamed result is held until its promise resolves, then hydrated; until then useAtomResult suspends on it instead of starting the atom, which would request it again. A streamed result that can't be decoded, or whose promise rejects, is dropped, and the atom runs in the browser.

See also

Loader

namespaceSource

Starts atoms from route loaders, before anything renders, so nothing waterfalls.

Each verb takes a source, an atom family or an AsyncResult atom, with its arguments. It starts the atom in the registry, takes a lease on it, so the registry's idle TTL can't evict it before the route renders and useAtomResult takes it over, and returns its handle for the component that reads it:

VerbStarts the atomWaitsHas a value already
prefetchif it isn't in the registryneverleaves it alone
ensureif it isn't in the registryonly while it's Initialreturns it at once
fetchif it isn't, or refreshes ituntil the new result settlesrefreshes, then waits

prefetchInBrowser is prefetch in the browser. On the server, it starts nothing, and the browser's read starts the atom.

Typed failures don't reject: ensure and fetch settle on them, and result returns the settled AsyncResult, so a loader can redirect on a tag and otherwise let a boundary render the failure. Only defects and interruptions reject.

The verbs are Effects that need the AtomRegistry; make gives the same verbs as Promises over one registry, for loaders that are async functions.

import { Loader } from "raf-effect-atom";

Loader.SettledResult

typemodelssince 0.1.0Source

type SettledResult<A, E> =
  | AsyncResult.Failure<A, E>
  | AsyncResult.Success<A, E>;

A settled AsyncResult: what ensure and fetch return.

Loader.LeaseTTL

constreferencessince 0.1.0Source

const LeaseTTL: Context.Reference<Duration.Duration>;

How long a loader's lease keeps an atom mounted when no component takes it over. Default 30 seconds.

Details

A lease ends when useAtomResult or useHandleValue mounts the atom, or when this time has passed, after which the registry's idle TTL applies as usual. While a component reads the atom, a verb takes no lease on it, so the atom goes idle when the last reader unmounts. On a request registry, leases last until the registry is disposed.

Loader.prefetch

constverbssince 0.1.0Source

const prefetch: <S extends Source>(
  source: S,
  ...args: ArgsOf<S>
) => Effect.Effect<Handle<S>, never, AtomRegistry.AtomRegistry>;

Starts a source's atom without waiting for it, and returns its handle.

When to use

Use for data the route needs but its loader doesn't: it streams in while the route renders.

Details

  • Starts the atom if it isn't in the registry, and leaves it alone if it is. Takes a lease either way, unless a mounted component reads the atom. The component that reads the handle suspends until the result settles. On a request registry whose async seed is still running, it waits for the seed first.
  • A derived atom that isn't serializable starts the first serializable atom its initialValueTarget chain points at, the one useAtomResult reads it through: the lease, the handle's key and what the server sends are that atom's. ensure and fetch wait for the derived atom's own result.

Gotchas

The server sends a result still loading as a promise only when the atom it sends holds an AsyncResult. A derived atom whose serializable atom holds another value, such as an Option snapshot, crosses with the value it has when the registry is dehydrated: point it at an AsyncResult atom, or ensure it.

Example

import { Effect } from "effect"
import { Loader } from "raf-effect-atom"

const program = Loader.prefetch(commentPage, issueId, 0)

Loader.prefetchInBrowser

constverbssince 0.2.6Source

const prefetchInBrowser: <S extends Source>(
  source: S,
  ...args: ArgsOf<S>
) => Effect.Effect<Handle<S>, never, AtomRegistry.AtomRegistry>;

Starts a source's atom in the browser, and returns its handle: on the server, it starts nothing, and the browser's read starts the atom.

When to use

Use for data only the browser can load: an input the server never sees, such as a token in the URL's fragment or a permission, or a request the server can't make. The rest of the page still renders on the server.

Details

  • In the browser, it's prefetch: a loader that runs there, such as on a client navigation, starts the atom and takes a lease.
  • On a request registry, it returns a handle marked inBrowser without starting the atom, so the atom needs no serialization key. On the server, useAtomResult throws for that handle, so React renders the nearest Suspense boundary's fallback there and renders the boundary in the browser. There, the read starts the atom, and the waterfall check skips it, since a loader asked for it.

Gotchas

  • Read the handle under a Suspense boundary: on the server, a read outside one fails the whole render. React reports the boundary it renders in the browser to onRecoverableError.
  • On a page the server rendered, the request starts when the browser renders the read, after hydration's JavaScript has loaded: start the data the server can load with prefetch or ensure.

Example

import { Loader } from "raf-effect-atom"

const program = Loader.prefetchInBrowser(pushSubscription)

Loader.ensure

constverbssince 0.1.0Source

const ensure: <S extends Source>(
  source: S,
  ...args: ArgsOf<S>
) => Effect.Effect<LoadedHandle<S>, never, AtomRegistry.AtomRegistry>;

Starts a source's atom, waits only while it has no result, and returns a handle whose reader renders without suspending.

When to use

Use when the loader needs the result, such as to redirect on a tag, or when the route shouldn't render without it.

Details

An atom with a result returns at once, without refetching. On a request registry, it also waits out a refresh in flight, so the server never renders a result that's about to change. A typed failure settles it like a success: result returns it. A defect or an interruption fails the Effect.

Example

import { Effect } from "effect"
import { AsyncResult } from "effect/reactivity"
import { Loader } from "raf-effect-atom"

const program = Effect.gen(function* () {
  const handle = yield* Loader.ensure(issue, 1)
  const result = Loader.result(handle)
  return AsyncResult.isSuccess(result) ? result.value.title : "missing"
})

Loader.fetch

constverbssince 0.1.0Source

const fetch: <S extends Source>(
  source: S,
  ...args: ArgsOf<S>
) => Effect.Effect<LoadedHandle<S>, never, AtomRegistry.AtomRegistry>;

Starts a source's atom, or refreshes it if it has a result, waits for the new result to settle, and returns a handle whose reader renders without suspending.

When to use

Use when the loader must not see a result from before it ran, such as after a mutation.

Details

Waits until the result settles and isn't refreshing. A typed failure settles it like a success: result returns it. A defect or an interruption fails the Effect.

Loader.result

constgetterssince 0.1.0Source

const result: <S extends Source>(
  handle: LoadedHandle<S>,
) => SettledResult<SuccessOf<S>, ErrorOf<S>>;

Returns the settled result ensure or fetch waited for.

When to use

Use in the loader that ran the verb, to branch on the result: redirect on a tag, or build another atom's arguments from it. Components read the handle with useAtomResult, which follows the registry.

Gotchas

Only the handle object the verb returned has it: a handle that crossed the network, as loader data or a server component's prop, is a copy, and this throws for it.

Example

import { AsyncResult } from "effect/reactivity"
import { Loader } from "raf-effect-atom"

const handle = await atoms.ensure(issue, id)
const error = AsyncResult.error(Loader.result(handle))

Loader.Loader

interfacemodelssince 0.1.0Source

interface Loader {
  /** {@link ensure}, as a Promise that rejects only on a defect. */
  readonly ensure: <S extends Source>(
    source: S,
    ...args: ArgsOf<S>
  ) => Promise<LoadedHandle<S>>;
  /** {@link fetch}, as a Promise that rejects only on a defect. */
  readonly fetch: <S extends Source>(
    source: S,
    ...args: ArgsOf<S>
  ) => Promise<LoadedHandle<S>>;
  /**
   * {@link prefetch}, run at once. On a request registry whose async seed is
   * still running, it returns the handle at once and starts the atom once
   * the seed resolves.
   */
  readonly prefetch: <S extends Source>(
    source: S,
    ...args: ArgsOf<S>
  ) => Handle<S>;
  /** {@link prefetchInBrowser}, run at once. */
  readonly prefetchInBrowser: <S extends Source>(
    source: S,
    ...args: ArgsOf<S>
  ) => Handle<S>;
  /**
   * Runs an Effect that needs the registry, such as the Effect verbs in an
   * `Effect.gen`, as a Promise that rejects with its squashed cause.
   * Aborting `signal`, or the one the verbs were made with, interrupts it,
   * and the Promise rejects with the abort reason.
   */
  readonly run: <A, E>(
    effect: Effect.Effect<A, E, AtomRegistry.AtomRegistry>,
    options?: {
      readonly signal?: AbortSignal | undefined;
    },
  ) => Promise<A>;
}

The loader verbs as Promises over one registry, as the framework integrations put them on the router context.

Loader.MakeOptions

interfacemodelssince 0.2.2Source

interface MakeOptions {
  /** How long a lease lasts in the browser. See {@link LeaseTTL}. */
  readonly leaseTTL?: Duration.Input | undefined;
  /**
   * The request's or the navigation's abort signal. Aborting it interrupts
   * the `ensure`, `fetch` and `run` calls still waiting, which reject with
   * its reason. The atoms they started keep loading.
   */
  readonly signal?: AbortSignal | undefined;
}

Options for make.

Loader.make

constconstructorssince 0.1.0Source

const make: (
  registry: AtomRegistry.AtomRegistry,
  options?: MakeOptions,
) => Loader;

Creates the Promise verbs over one registry.

When to use

Use in a framework integration, in loaders that are async functions, or in an event handler that starts an atom before the state change that renders its reader, for a choice a click or a submit makes that doesn't belong in the URL. In Effect code, use the verbs directly, and run the Effect on the registry with run.

Details

With a signal, an aborted request or navigation stops waiting: ensure, fetch and run reject with the signal's reason, and the atoms they started keep loading. The React Router and Next.js integrations pass the request's.

Example

import { Loader, RequestRegistry } from "raf-effect-atom"

const loader = Loader.make(RequestRegistry.make(), { signal: request.signal })
const comments = loader.prefetch(commentPage, 1, 0)
const issueHandle = await loader.ensure(issue, 1)

RequestRegistry

namespaceSource

A registry per server request.

@effect/atom-react's default RegistryContext is one registry for the whole module, which on a server would share one user's results with every other request. Each framework integration makes a request registry with make, provides it through RegistryContext, and dehydrates it into the response.

import { RequestRegistry } from "raf-effect-atom";

RequestRegistry.Seed

typemodelssince 0.2.0Source

type Seed = Seeds.Seed;

Prepares a request registry before the loader verbs start atoms in it, such as by setting an atom to the request's cookie.

RequestRegistry.InitialValues

typemodelssince 0.2.4Source

type InitialValues = Iterable<
  readonly [Atom.Atom<any>, any, Atom.InitialValueOptions?]
>;

Atoms paired with the values a registry starts with, as AtomRegistry.make's initialValues takes them: build each pair with Atom.initialValue.

RequestRegistry.Options

interfacemodelssince 0.2.0Source

interface Options {
  /**
   * Values the registry starts with, as `AtomRegistry.make`'s
   * `initialValues`. Unlike `registry.set` in a seed, they reach readable
   * atoms, such as one that provides a per-request service. The registry
   * keeps each of these atoms mounted until it's disposed, so the value
   * outlives the atoms that read it.
   */
  readonly initialValues?: InitialValues | undefined;
  /**
   * Runs on the new registry before any verb starts an atom in it. When it
   * returns a promise, `ensure` and `fetch` wait for it, and `prefetch`
   * returns its handle at once and starts the atom once it resolves.
   */
  readonly seed?: Seed | undefined;
}

Options for make.

RequestRegistry.make

constconstructorssince 0.1.0Source

const make: (options?: Options) => AtomRegistry.AtomRegistry;

Creates the registry for one server request.

Details

  • It's an ordinary AtomRegistry, marked as a request registry. On a request registry, ensure waits like fetch does, nothing revalidates in the background, and a loader's lease lasts until the registry is disposed. In development, the loader verbs refuse an atom that isn't serializable, because the browser would request it again.
  • initialValues are values the request's atoms start with, set on each atom, or on the end of its initialValueTarget chain, before anything reads it. They reach atoms registry.set can't, such as a readable atom that provides the request's fetch, and each stays mounted until the registry is disposed.
  • seed sets what the request's atoms need before they run, such as the page request's cookie for an RPC protocol that forwards it. It can be async, for request data only a promise gives, such as Next.js's headers(). It runs after initialValues are set.

Gotchas

With an async seed, a prefetch before it settles starts its atom later, so Hydration.dehydrate would miss it: wait for settled first, as the framework integrations do.

Example

import { Atom } from "effect/reactivity"
import { RequestRegistry } from "raf-effect-atom"

const registry = RequestRegistry.make({
  initialValues: [Atom.initialValue(requestFetch, request.fetch)],
  seed: (registry) => registry.set(requestCookie, request.headers.get("cookie") ?? undefined)
})

RequestRegistry.settled

constgetterssince 0.2.0Source

const settled: (registry: AtomRegistry.AtomRegistry) => Promise<void>;

Waits until a request registry's seed has settled and every atom a verb started while it ran has started.

When to use

Use before Hydration.dehydrate on a registry with an async seed, outside the framework integrations, which call it for you.

Details

Resolves at once for a registry without a seed, or whose seed was synchronous. Rejects with the seed's error.

RequestRegistry.layer

constlayerssince 0.1.0Source

const layer: Layer.Layer<AtomRegistry.AtomRegistry>;

A layer that provides a request registry as the AtomRegistry, disposed when the layer's scope closes.

When to use

Use when the request is handled in Effect, around the loaders' verbs and dehydrate.

See also

  • make for the registry it provides

RequestRegistry.isRequestRegistry

constguardssince 0.1.0Source

const isRequestRegistry: (
  registry: AtomRegistry.AtomRegistry,
) => boolean;

Returns whether a registry was made by make, for one server request.

Use atom result

The read hooks for atoms started by loaders and streamed from the server.

useAtomResult

consthookssince 0.1.0Source

const useAtomResult: <S extends Source>(
  source: S,
  handle: NoInfer<Handle<Readable<S>>>,
) => [ErrorOf<S>] extends [never]
  ? AsyncResult.Success<SuccessOf<S>, never>
  : SettledResult<SuccessOf<S>, ErrorOf<S>>;

Reads a handle through React Suspense, through the source that made it: a result streamed from the server, a hydrated one, or one a loader started.

When to use

Use in the component a loader, a server component or useWaterfall gave the handle to. The source can be the one the verb started, or one derived from it whose atoms point at its atoms with initialValueTarget. To send some failures to an error boundary, pass the result to the boundary's hook, such as useIssueBoundary(useAtomResult(issue, handle)).

Details

  • The atom is source(...handle.args), or source itself for an atom. In development, the read throws when the atom it hydrates has another serialization key than the handle's: the handle came from another source.
  • A handle from ensure or fetch renders without suspending, unless the registry dropped the result since; one from prefetch or useWaterfall suspends until the result settles. The registry is the source of truth either way: the read follows refreshes and mutations.
  • A result the server was still loading when it dehydrated is awaited instead of started, so the browser doesn't request it a second time.
  • The atom is read through hydrated, so a hydrated result is kept.
  • Once the component mounts, the loader's lease ends and the component keeps the atom mounted. A loader that starts the atom again while it's mounted takes no lease, so the atom goes idle when the component unmounts.
  • A derived atom is read through the first serializable atom its initialValueTarget chain points at: that one is awaited, hydrated and released, so the derived atom doesn't request it again.
  • The settled result is returned, typed failures included. A defect is thrown to the error boundaries above. With no typed failures, it's typed as a success.
  • While a failed atom refreshes, such as after a boundary's reset, the read suspends instead of returning the old failure again.

Gotchas

A read of an atom that nothing started, such as one whose loader result the registry dropped, starts it, and its request waits for the render: a request waterfall. In development, the read reports it as WaterfallDetection says, by default with a warning. Start the atom in a loader or a server component, with the state that picks it in the URL, and pass its handle down. For state that can't be in the URL, start it in the event handler that changes the state, or make its handle with useWaterfall when only the render knows its input.

Two sources with the same arguments and success type take each other's handles as far as the types go; the development check catches it.

During server rendering, the registry must be a request registry (see RequestRegistry.make), or a module-level registry would share results between requests: in development, the hook throws without one.

On the server, the hook throws for a handle Loader.prefetchInBrowser returned, so React renders the nearest Suspense boundary's fallback and reports the error. With no boundary between the read and the route, the server can't render the page. useHandleValue returns Initial there instead.

Atom.transform's initialValueTarget option points at the end of the target's own chain, which for an AtomRpc query with reactivityKeys is an inner atom with no serialization key. A derived atom read here sets initialValueTarget to its source itself, or it requests the source again after hydration.

Example

import { AsyncResult } from "effect/reactivity"
import { type Handle, useAtomResult } from "raf-effect-atom"

function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
  const result = useAtomResult(issue, props.issue)
  return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
}

See also

  • hydrated for reading hydrated atoms with Atom's own hooks

useHandleValue

consthookssince 0.3.1Source

const useHandleValue: <S extends Source>(
  source: S,
  handle: NoInfer<Handle<Readable<S>>>,
) => AsyncResult.AsyncResult<SuccessOf<S>, ErrorOf<S>>;

Reads a handle without React Suspense, through the source that made it, and returns the atom's current result as it is.

When to use

Use where a component renders every state of the read itself: its loading state, a failure that stays on the page while the atom refreshes, or a defect. Otherwise prefer useAtomResult, which suspends until the result settles and sends defects to the error boundaries above.

Details

  • It reads the atom as useAtomResult does: through hydrated, through the serializable atom a derived source points at, and with the same development checks on the handle's key and on request waterfalls.
  • It returns Initial while the atom loads, and while a result the server was still loading streams in. It doesn't read the atom until the stream settles, so the browser doesn't request it a second time.
  • A failure is returned as it is, waiting while the atom refreshes, and so is a defect.
  • Once the component has subscribed to the atom, the loader's lease ends and the component keeps the atom mounted. A loader that starts the atom again while it's mounted takes no lease, so the atom goes idle when the component unmounts.
  • A handle Loader.prefetchInBrowser returned on the server reads as Initial there, without starting the atom; the browser's read starts it.

Gotchas

The server renders what the read returns, Initial for a result still loading, and the browser's first render has to match it: render the same loading state for every Initial.

During server rendering, the registry must be a request registry (see RequestRegistry.make): in development, the hook throws without one.

Example

import { AsyncResult } from "effect/reactivity"
import { type Handle, useHandleValue } from "raf-effect-atom"

function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
  const result = useHandleValue(issue, props.issue)
  return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : <p>Loading…</p>
}

See also

  • useAtomResult for a read that suspends until the result settles

Hydration boundary

Applies a server registry's dehydrated state to the nearest RegistryContext, while rendering.

HydrationBoundaryProps

typemodelssince 0.1.0Source

type HydrationBoundaryProps =
  | {
      readonly children?: React.ReactNode | undefined;
      /**
       * The request registry of the server component render, such as
       * `raf-effect-atom/rsc`'s `getRegistry()`. Server components only.
       */
      readonly registry: AtomRegistry.AtomRegistry;
      readonly state?: never;
    }
  | {
      readonly children?: React.ReactNode | undefined;
      readonly registry?: never;
      /** The state `Hydration.dehydrate` returned on the server. */
      readonly state?: Iterable<Hydration.DehydratedAtom> | undefined;
    };

Props for HydrationBoundary: the request's registry in a server component, or the state it dehydrated anywhere else.

HydrationBoundary

constcomponentssince 0.1.0Source

const HydrationBoundary: (
  props: HydrationBoundaryProps,
) => React.ReactNode;

Hydrates the state a server registry dehydrated, streamed results included.

When to use

Use where the server's state reaches the React tree through rendering rather than a router: a server component's registry, or a React Router loader's data. A router that hydrates before rendering calls Hydration.hydrate instead.

Details

In a server component, pass the request's registry. Server components get their own build of HydrationBoundary through the package's react-server export condition: it waits for the registry's seed, dehydrates it, and sends the state with the server component's payload, so results still loading stream in as they settle.

Anywhere else, pass the state a loader dehydrated. Settled values go to @effect/atom-react's HydrationBoundary, which hydrates new atoms during render and existing ones after commit. Each streamed result is held until it arrives, so useAtomResult waits for it instead of requesting it again.

During server rendering, when state is the array dehydrate returned, as React Router passes a loader's data to the server render, the children read the registry it came from instead of a copy. The loaders' atoms are there, so an effect-atom-boundary boundary sees their failures before its children read them and renders its handler on the server.

Gotchas

registry works only in server components: in a client component it throws, since a registry can't cross to the browser. In development, it throws during server rendering when the registry isn't a request registry, such as @effect/atom-react's default one, which every request shares: one request's state would reach the others.

Example

import { HydrationBoundary } from "raf-effect-atom"
import { ensure, getRegistry } from "raf-effect-atom/rsc"

// A server component
export default async function IssueRoute(props: { params: Promise<{ id: string }> }) {
  const handle = await ensure(issue, Number((await props.params).id))
  return (
    <HydrationBoundary registry={getRegistry()}>
      <IssuePage issue={handle} />
    </HydrationBoundary>
  )
}

// A React Router route component
<HydrationBoundary state={loaderData.atoms}>
  <IssuePage issue={loaderData.issue} />
</HydrationBoundary>

Registry provider

The registry provider for apps that render the server's state through HydrationBoundary, such as React Server Components and React Router.

RegistryProvider

constcomponentssince 0.1.0Source

const RegistryProvider: (props: {
  readonly children?: React.ReactNode | undefined;
  readonly defaultIdleTTL?: number | undefined;
}) => React.ReactNode;

Provides the app's registry: a request registry while the framework renders the app on the server, and one registry for the page's lifetime in the browser.

When to use

Put it in the root layout, around children, in place of @effect/atom-react's RegistryProvider, which would share one registry between requests' server renders.

Details

defaultIdleTTL is how many milliseconds the browser's registry keeps an atom no component reads, 400 by default, as @effect/atom-react's default registry does. An atom's own Atom.setIdleTTL or Atom.keepAlive wins. It's read once, when the registry is made. The server's request registry lives for one request, so it has none.

In the browser, React Router's clientAtomsMiddleware gives client loaders the registry of the mounted provider.

Example

import { RegistryProvider } from "raf-effect-atom"

export default function RootLayout(props: { children: React.ReactNode }) {
  return (
    <RegistryProvider defaultIdleTTL={60_000}>{props.children}</RegistryProvider>
  )
}

Waterfalls

Request waterfalls: the escape hatch for atoms only a render can start, and the development check that finds the others.

WaterfallMode

typemodelssince 0.1.0Source

type WaterfallMode = Waterfalls.Mode;

What useAtomResult does in development when a read starts an atom that no loader, server component, event handler or useWaterfall started.

  • "warn": console.warn once per atom, naming it. The default.
  • "throw": throw to the nearest error boundary, so the page can't be used until the atom starts before render.
  • "off": don't check.

WaterfallDetection

constcomponentssince 0.1.0Source

const WaterfallDetection: (props: {
  readonly children?: React.ReactNode | undefined;
  readonly mode: WaterfallMode;
}) => React.ReactNode;

Sets how useAtomResult reports request waterfalls in the tree below.

When to use

Put it in the root layout, around the app. "throw" turns every atom a component starts on its own into an error in development, which a person or a coding agent can't miss.

Details

Production builds don't check: the check sits behind process.env.NODE_ENV !== "production", which bundlers replace at build time, so it's left out of the bundle, and this component renders only its children.

Example

import { WaterfallDetection } from "raf-effect-atom"

function App(props: { readonly children: React.ReactNode }) {
  return <WaterfallDetection mode="throw">{props.children}</WaterfallDetection>
}

useWaterfall

consthookssince 0.1.0Source

const useWaterfall: <S extends Source>(
  source: S,
  ...args: ArgsOf<S>
) => Handle<S>;

Starts a source's atom during render and returns its handle, for an input only a render knows, such as an id read from another atom's result.

When to use

As an escape hatch, when no loader, server component or event handler can start the atom: each call is a request waterfall, a round trip after the render that found the input. Prefer putting the state that picks the data in the URL, where a loader reads it, or a procedure that resolves the input on the server. Searching a codebase for useWaterfall lists its waterfalls.

Details

  • Starts the atom like Loader.prefetch, with a lease that useAtomResult takes over when the component mounts, and only if nothing started it yet: re-renders start nothing.
  • A result the server is still streaming is awaited by useAtomResult, not started again.
  • useAtomResult doesn't report an atom started this way.
  • The handle is a prefetched one: its reader suspends until the result settles, as for a handle from a loader's prefetch.
  • A derived source starts the serializable atom its initialValueTarget chain points at, as Loader.prefetch does.

Example

import { useAtomResult, useWaterfall } from "raf-effect-atom"

function Author(props: { readonly userId: UserId }) {
  const result = useAtomResult(user, useWaterfall(user, props.userId))
  // ...
}

See also