Getting started
For coding agents
What to write and what to avoid when a coding agent builds with raf-effect-atom and effect-atom-boundary, with the reason for each rule and the page that explains it.
These rules are for coding agents writing code with raf-effect-atom and effect-atom-boundary, and for the people reviewing that code. Each one keeps a page's requests starting before it renders, its results crossing from the server once, and its failures typed.
Do
- Start every atom before render. Start it in the route's loader or the server component with the loader verbs (
atoms.prefetch(issue, id),atoms.ensure(issue, id)). See Request waterfalls. - Pass handles, not ids. A component that reads an atom takes the handle its loader or server component returned, typed
Handle<typeof issue>, and reads it withuseAtomResult(issue, props.issue). A component given an id can start the request as it renders. See Handles. - Put the state that picks data in the URL. A page number, a filter, the selected item or a dialog that shows data goes in search params, or for a dialog in a route of its own (route masking, parallel routes), so the loader starts its atoms and links, reload and back work. See Put the state in the URL.
- Start every atom before awaiting any. In a loader,
prefetcheverything first, thenawaitwhat the loader needs. GiveEffect.allaconcurrency, or it runs its effects one after another. See Start every atom before awaiting any. - Read with
useAtomResult. It waits for a result the server is still streaming, keeps a hydrated one, and checks for waterfalls in development. A component that renders its own loading state and defects reads withuseHandleValue(issue, props.issue), which does the same without suspending. See Reading atoms. - Give every atom a loader starts a
serializationKey. Only serializable atoms cross from the server to the browser; without a key, the browser requests the atom again. See Streaming and hydration. - Start browser-only reads with
prefetchInBrowser. A token in the URL fragment, a credential insessionStorageor a browser API never reaches the server. Read it inside the atom's effect, start the atom in the loader withatoms.prefetchInBrowser(source, ...args), and read its handle under a Suspense boundary: the server renders the fallback and the browser's read starts it. It needs noserializationKey. Don't set the input in an atom from auseEffect. See Inputs only the browser has. - Hydrate through one
HydrationBoundary. Import it fromraf-effect-atomin server and client code alike. In a server component, pass the request's registry (<HydrationBoundary registry={atoms.getRegistry()}>); in a React Router route, pass the loader's state (<HydrationBoundary state={loaderData.atoms}>). See HydrationBoundary. - Catch
RpcClientErroronce, around the app. Every procedure fails with it when the request doesn't reach the server or the answer doesn't decode. Claim it in a rootcreateErrorBoundarywhose handler offersreset, and chain the app's other boundaries under it withparent. Pass each read's result to the nearest boundary's hook,useIssueBoundary(useAtomResult(issue, props.issue)), so the component is left with the contract's errors. AnAtomHttpApiclient makes its transport failures defects instead: leave them to the route's error component, not acatchTagshandler. See Shared boundaries. - Handle each failure by its tag. Catch tags in a boundary's
catchTags, or check_tagon what's left in the component. One message for every failure hides which one happened. See Error boundaries. - Keep
<WaterfallDetection mode="throw">at the root. In development, a read that starts an atom nothing started then throws to the nearest error boundary, naming the atom and the fix. Fix the cause the report names. See Finding waterfalls in development. - Put a route's inputs in its URL. An atom whose input comes from another atom's result waits for that result. Put the input in the path, or have the backend resolve the relation, before reaching for
useWaterfall. See Dependent atoms, and Shapes that force waterfalls for gating shells, per-item reads and data that arrives later.
Avoid
- Fetching in components. No
useEffectthat calls the client orregistry.mount, noEffect.runPromisein render, and no handle built by hand for an atom nothing starts. Each one requests only after the component renders. - Atom's own hooks on server data.
useAtomValueanduseAtomSuspenseneither wait for a result the server is still streaming nor keep a hydrated one, so the browser requests it again, and they skip the waterfall check. - Escape hatches for state the URL could hold. Starting an atom in an event handler (
Loader.make(registry).prefetch) or withuseWaterfallis for an input no loader can know: a choice a click makes that doesn't belong in the URL, or an id only a render finds. A search box's text goes in the URL, so the loader starts the search. See Escape hatches and Search as you type. useWaterfallto silence a report. It's for an input that exists only in render, such as an id read from another atom's result, and each call is a round trip. On typed input it requests every value. If a loader or handler can know the input, start the atom there.mode="off"ormode="warn"to get past a report. The report is the waterfall: turning it off ships it.awaitbefore the next start.await atoms.ensure(issue, id)followed byatoms.prefetch(assignee, id)is two round trips; swap the lines and it's one.- Catch-all failure handling. Don't branch on
error.message, and don't render one message for a failure that can be several tags. The error type names every tag the read can fail with.