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

raf-effect-atom

Reading atoms

useAtomResult reads an atom a loader started or the server streamed, waits for it under Suspense, and returns its typed failures. useHandleValue reads it without suspending.

Components read the atoms their loader started with raf-effect-atom's useAtomResult(source, handle). It takes the source the loader passed to its verb and the handle the verb returned, suspends like useAtomSuspense until the atom has a result, and returns it:

src/routes/issues.$id.tsx
function function IssuePage(): React.JSX.ElementIssuePage() {
	const 
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
=
const Route: Route<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
    readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/issues/$id", "/issues/$id">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/issues/$id", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
// IssueNotFound, Unauthorized and RpcClientError go to their boundaries, // so no failure is left. const result = useIssueBoundary<SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>): Success<Issue, never>useIssueBoundary(useAtomResult<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(id: number & Brand<"IssueId">) => Atom<AsyncResult<Issue, unknown>>>>): SettledResult<Issue, RpcClientError | ... 1 more ... | Unauthorized>useAtomResult(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
.issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue));
const result: Success<Issue, never>
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const result: Success<Issue, never>result.Success<Issue, never>.value: Issuevalue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading the assignee…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function Assignee(props: {
    assignee: Handle<typeof assignee>;
}): React.JSX.Element
Assignee
assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
.assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } // Takes the handle the loader returned, not an id: it reads a request that // already started, and can't start its own. function
function Assignee(props: {
    assignee: Handle<typeof assignee>;
}): React.JSX.Element
Assignee
(
props: {
    assignee: Handle<typeof assignee>;
}
props
: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: interface Handle<S>Handle<typeof const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) {
const const result: Success<User, never>result = useIssueBoundary<SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>): Success<User, never>useIssueBoundary(useAtomResult<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(issueId: number & Brand<"IssueId">) => Atom<AsyncResult<User, unknown>>>>): SettledResult<...>useAtomResult(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
props: {
    assignee: Handle<typeof assignee>;
}
props
.assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee));
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const result: Success<User, never>result.Success<User, never>.value: Uservalue.name: stringname}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; }

Handles

A component that reads an atom takes its handle as a prop, not the id it was built from. A component given an id could build the atom and start the request as it renders; given a handle, it reads a request a loader, a server component or an event handler already started. The handle's type names its source, so Handle<typeof assignee> takes no id and no other family's handle.

A handle is plain data, so it goes through loader data and server component props unchanged. In development, useAtomResult throws when the atom the source makes from the handle's arguments has a different key from the handle's, such as a handle passed with the wrong source.

An event handler makes a handle with Loader.make(registry).prefetch(source, ...args), and keeps it in state for the component that reads it. A value that exists only in render, such as an id read from another atom's result, gets one from useWaterfall.

Failures

useAtomResult(source, handle) returns the settled result, typed failures included: check it with AsyncResult.isFailure or AsyncResult.match. A defect or an interruption isn't a typed failure: it throws Cause.squash(cause) to the error boundaries above.

To send some failures to an effect-atom-boundary boundary, pass the result to its hook: useIssueBoundary(useAtomResult(issue, props.issue)) throws the claimed failures to their boundaries and returns the result typed without them. When the boundaries claim every failure, as the issue route's do, no typed failure is left and the result is typed as a success: the component reads result.value without a check. raf-effect-atom doesn't import effect-atom-boundary.

While a failed atom refreshes, such as after a boundary's reset, the read suspends instead of returning the old failure again.

What it adds to Atom's own read

  • A result the server was still loading when it sent the page is awaited, not started, so the browser doesn't request it a second time.
  • It reads through Hydration.hydrated(atom), so a hydrated result is kept on the first read instead of running the atom's effect again.
  • Once the component mounts, the loader's lease ends and the component keeps the atom mounted. A loader that starts the atom again while the component is mounted takes no lease, so the atom goes idle when the last component reading it unmounts.
  • A derived atom that points at another with Effect's initialValueTarget is read through that atom: the streamed result, the hydrated copy and the lease are the source's.

Reading without Suspense

useHandleValue(source, handle) takes the same source and handle and returns the atom's current result without suspending, for a component that renders every state of the read itself:

  • 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 as it is, waiting while the atom refreshes, instead of suspending.
  • A defect or an interruption as a failure with its cause, instead of throwing it.

It reads through Hydration.hydrated(atom) and through the atom a derived source points at, ends the loader's lease once it has subscribed, and checks the handle's key and waterfalls in development, as useAtomResult does. A handle prefetchInBrowser returned reads as Initial on the server, without starting the atom.

The server renders the Initial of a result still loading, so render the same loading state for every Initial or the browser's first render won't match it. Where a Suspense fallback and the error boundaries above can show the loading state and the defects, use useAtomResult.

On the server

Server rendering needs a registry per request, or one user's results would reach another. In development, useAtomResult, useHandleValue and HydrationBoundary throw during server rendering when the registry isn't a request registry, such as @effect/atom-react's default one, which every request shares. The framework integrations provide one; outside them, provide RequestRegistry.make() through RegistryContext.