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
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
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
type SuccessOf<S> =
ResultOf<S> extends AsyncResult.AsyncResult<infer A, infer _E>
? A
: never;The success type of a source's AsyncResult.
ErrorOf
type ErrorOf<S> =
ResultOf<S> extends AsyncResult.AsyncResult<infer _A, infer E>
? E
: never;The error type of a source's AsyncResult.
HandleKey
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
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
LoadedHandlefor the handleensureandfetchreturn
LoadedHandle
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
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
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.serializablefor the metadata it copies
Hydration.dehydrate
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
hydratefor the browser side
Hydration.hydrate
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
dehydratefor the server side
Loader
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:
| Verb | Starts the atom | Waits | Has a value already |
|---|---|---|---|
prefetch | if it isn't in the registry | never | leaves it alone |
ensure | if it isn't in the registry | only while it's Initial | returns it at once |
fetch | if it isn't, or refreshes it | until the new result settles | refreshes, 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
type SettledResult<A, E> =
| AsyncResult.Failure<A, E>
| AsyncResult.Success<A, E>;A settled AsyncResult: what ensure and fetch return.
Loader.LeaseTTL
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
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
initialValueTargetchain points at, the oneuseAtomResultreads it through: the lease, the handle's key and what the server sends are that atom's.ensureandfetchwait 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
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
inBrowserwithout starting the atom, so the atom needs no serialization key. On the server,useAtomResultthrows 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
prefetchorensure.
Example
import { Loader } from "raf-effect-atom"
const program = Loader.prefetchInBrowser(pushSubscription)Loader.ensure
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
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
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
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
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
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
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
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
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
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
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,ensurewaits likefetchdoes, 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. initialValuesare values the request's atoms start with, set on each atom, or on the end of itsinitialValueTargetchain, before anything reads it. They reach atomsregistry.setcan't, such as a readable atom that provides the request'sfetch, and each stays mounted until the registry is disposed.seedsets 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'sheaders(). It runs afterinitialValuesare 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
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
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
makefor the registry it provides
RequestRegistry.isRequestRegistry
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
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), orsourceitself 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
ensureorfetchrenders without suspending, unless the registry dropped the result since; one fromprefetchoruseWaterfallsuspends 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
initialValueTargetchain 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
hydratedfor reading hydrated atoms with Atom's own hooks
useHandleValue
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
useAtomResultdoes: throughhydrated, 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
Initialwhile 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,
waitingwhile 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.prefetchInBrowserreturned on the server reads asInitialthere, 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
useAtomResultfor a read that suspends until the result settles
Hydration boundary
Applies a server registry's dehydrated state to the nearest
RegistryContext, while rendering.
HydrationBoundaryProps
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
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
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
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.warnonce 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
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
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 thatuseAtomResulttakes 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. useAtomResultdoesn'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
initialValueTargetchain points at, asLoader.prefetchdoes.
Example
import { useAtomResult, useWaterfall } from "raf-effect-atom"
function Author(props: { readonly userId: UserId }) {
const result = useAtomResult(user, useWaterfall(user, props.userId))
// ...
}See also
WaterfallDetectionfor the development check it opts out of