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
Early · 0.x

Render-as-you-fetch and typed error boundaries for Effect Atom

Start atoms from route loaders, stream them into the browser with their typed failures intact, and catch those failures by tag in React. Two thin layers on Effect Atom and Effect RPC.

src/routes/issues.$id.tsx
export const 
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
=
createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$id")({
params?: {
    parse?: ParseParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
    priority?: number;
    stringify?: StringifyParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
} | undefined
params
: {
parse?: ParseParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
parse
: (
params: {
    id: string;
}
params
) => import SchemaSchema.
function decodeUnknownSync<Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
    readonly id: number & Brand<"IssueId">;
}
decodeUnknownSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
)(
params: {
    id: string;
}
params
),
stringify?: StringifyParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
stringify
: import SchemaSchema.
const encodeSync: <Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions) => (input: {
    readonly id: number & Brand<"IssueId">;
}, options?: ParseOptions) => {
    readonly id: string;
}
encodeSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
),
},
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader; }, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async ({
context: {
    atoms: Loader;
}
context
,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
}) => ({
// Starts the assignee; it streams in while the page renders. assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:
context: {
    atoms: Loader;
}
context
.atoms: Loaderatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id),
// Waits for the issue, so the page renders with it. issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await
context: {
    atoms: Loader;
}
context
.atoms: Loaderatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id),
}), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: () => ( <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <function IssuePage(): React.JSX.ElementIssuePage /> </const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> ), }); 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>; }
src/app/issues/[id]/page.tsx
export default async function 
function Page(props: {
    params: Promise<{
        id: string;
    }>;
}): Promise<React.JSX.Element>
Page
(
props: {
    params: Promise<{
        id: string;
    }>;
}
props
: {
params: Promise<{
    id: string;
}>
params
: interface Promise<T>Promise<{ id: stringid: string }> }) {
const const id: number & Brand<"IssueId">id = import SchemaSchema.const decodeSync: <Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>>(schema: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>, options?: ParseOptions) => (input: string, options?: ParseOptions) => number & Brand<"IssueId">decodeSync(const IssueIdFromString: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>IssueIdFromString)((await
props: {
    params: Promise<{
        id: string;
    }>;
}
props
.
params: Promise<{
    id: string;
}>
params
).id: stringid);
// Starts the assignee; it streams in while the page renders. const const assigneeHandle: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle = prefetch<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">): Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id); // Waits for the issue, so the page renders with it. const const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle = await ensure<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">): Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id); return ( // The request registry's results go to the browser, still-loading ones // as promises. <const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary registry: AtomRegistryregistry={function getRegistry(): AtomRegistrygetRegistry()}> <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <
function IssuePage(props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePage
assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={const assigneeHandle: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle} issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue={const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle} />
</const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> </const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary> ); }
src/components/issue-page.tsx
"use client";
export function 
function IssuePage(props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePage
(
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
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>; issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue>; }) { // 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,
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(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={
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
props
.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 server component passed, 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>; }
app/routes/issues.$id.tsx
async function 
function load({ context, params }: Route.LoaderArgs): Promise<{
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
}>
load
({ context: Readonly<RouterContextProvider>context,
params: {
    id: string;
}
params
}: Route.
type Route.LoaderArgs = {
    request: Request;
    url: URL;
    params: {
        id: string;
    };
    pattern: string;
    context: Readonly<RouterContextProvider>;
}
LoaderArgs
) {
const const id: number & Brand<"IssueId">id = import SchemaSchema.const decodeSync: <Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>>(schema: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>, options?: ParseOptions) => (input: string, options?: ParseOptions) => number & Brand<"IssueId">decodeSync(const IssueIdFromString: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>IssueIdFromString)(
params: {
    id: string;
}
params
.id: stringid);
const const atoms: RequestAtomsatoms = context: Readonly<RouterContextProvider>context.get<RequestAtoms>(context: RouterContext<RequestAtoms>): RequestAtomsget(const atomsContext: RouterContext<RequestAtoms>atomsContext); const
const handles: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
handles
= {
// Starts the assignee; it streams in while the page renders. assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: const atoms: RequestAtomsatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id), // Waits for the issue, so the page renders with it. issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await const atoms: RequestAtomsatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id), }; // The registry's results go to the browser, still-loading ones as promises. return { atoms: DehydratedAtom[]atoms: const atoms: RequestAtomsatoms.RequestAtoms.dehydrate: () => Array<DehydratedAtom>dehydrate(),
handles: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
handles
};
} // The first page loads on the server; after it, the same function runs in // the browser, where `ensure` finds what the registry already holds. export const
const loader: ({ context, params }: Route.LoaderArgs) => Promise<{
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
}>
loader
=
function load({ context, params }: Route.LoaderArgs): Promise<{
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
}>
load
;
export const
const clientLoader: ({ context, params }: Route.LoaderArgs) => Promise<{
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
}>
clientLoader
=
function load({ context, params }: Route.LoaderArgs): Promise<{
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
}>
load
;
export default function function Page({ loaderData }: Route.ComponentProps): React.JSX.ElementPage({
loaderData: {
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
} | {
    atoms: {
        readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
    }[];
    handles: {
        ...;
    };
}
loaderData
}: Route.
type Route.ComponentProps = {
    params: {
        id: string;
    };
    matches: [{
        id: "root";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: undefined;
        handle: unknown;
    }, {
        id: "routes/issues.$id";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: {
            atoms: DehydratedAtom[];
            handles: {
                assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
                issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
            };
        } | {
            ...;
        };
        handle: unknown;
    }, ...({
        ...;
    } | undefined)[]];
} & {
    ...;
}
ComponentProps
) {
return ( <const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary state?: Iterable<DehydratedAtom> | undefinedstate={
loaderData: {
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
} | {
    atoms: {
        readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
    }[];
    handles: {
        ...;
    };
}
loaderData
.
atoms: DehydratedAtom[] | {
    readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
}[]
atoms
}>
<const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <
function IssuePage(props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePage
{...
loaderData: {
    atoms: DehydratedAtom[];
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    };
} | {
    atoms: {
        readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
    }[];
    handles: {
        ...;
    };
}
loaderData
.
handles: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
} | {
    assignee: {
        readonly args: [issueId: number & Brand<"IssueId">];
        readonly inBrowser?: true | undefined;
        readonly key: HandleKey<...> | undefined;
    };
    issue: {
        ...;
    };
}
handles
} />
</const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> </const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary> ); } function
function IssuePage(props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePage
(
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
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>; issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue>; }) { // 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,
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(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={
props: {
    assignee: Handle<typeof assignee>;
    issue: Handle<typeof issue>;
}
props
.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>; }

How a route loads its data

A page says what it needs before it renders. The atoms are made once from the RPC client, the route’s loader starts every request as the navigation begins, components read the results as they arrive, and boundaries catch the failures by tag.

  1. Make atoms from the RPC client

    AtomRpc.Service turns the RPC contract into atoms: IssuesClient.query("GetIssue", { id }) is an atom of that call’s AsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.

    #rpc-transport picks where requests go: during server rendering they call the RPC handler in the same process, and in the browser they POST /rpc, a server route.

    Quick start
    src/issues-client.ts
    export class class IssuesClientIssuesClient extends import AtomRpcAtomRpc.
    const Service: <IssuesClient>() => <Id, Rpcs, ER, RM>(id: Id, options: {
        readonly group: RpcGroup<Rpcs>;
        readonly protocol: Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never> | ((get: AtomContext) => Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never>);
        readonly spanPrefix?: string | undefined;
        readonly spanAttributes?: Record<string, unknown> | undefined;
        readonly generateRequestId?: (() => RequestId) | undefined;
        readonly disableTracing?: boolean | undefined;
        readonly makeEffect?: Effect<...> | undefined;
        readonly runtime?: RuntimeFactory | undefined;
    }) => AtomRpc.AtomRpcClient<...>
    Service
    <class IssuesClientIssuesClient>()(
    "IssuesClient", {
    group: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    group
    :
    const IssueRpcs: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    IssueRpcs
    ,
    protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol: import RpcClientRpcClient.
    const layerProtocolHttp: (options: {
        readonly url: string;
        readonly transformClient?: <E, R>(client: HttpClient.With<E, R>) => HttpClient.With<E, R>;
        readonly batch?: {
            readonly window: Input;
            readonly maxSize?: number | undefined;
        } | undefined;
    }) => Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>
    layerProtocolHttp
    ({ url: stringurl: const rpcUrl: "http://in-process/rpc"rpcUrl }).Pipeable.pipe<Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, Layer.Layer<RpcClient.Protocol, never, never>>(this: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, ab: (_: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>) => Layer.Layer<RpcClient.Protocol, never, never>): Layer.Layer<...> (+21 overloads)pipe(
    import LayerLayer.const provide: <[Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]>(that: [Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, RpcSerialization.RpcSerialization | HttpClient>> (+3 overloads)provide([const RpcHttpClient: Layer.Layer<HttpClient, never, never>RpcHttpClient, import RpcSerializationRpcSerialization.const layerJson: Layer.Layer<RpcSerialization.RpcSerialization, never, never>layerJson]), ), }, ) {}
    src/issues.ts
    // A serialization key lets the server send the result to the browser.
    export const issue = (id: number & Brand<"IssueId">id: type IssueId = number & Brand<"IssueId">IssueId) =>
    const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssue">(tag: "GetIssue", payload: {
        readonly id: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    query
    ("GetIssue", { id: number & Brand<"IssueId">id }, { serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` });
    export const const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId: type IssueId = number & Brand<"IssueId">IssueId) => class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssueAssignee">(tag: "GetIssueAssignee", payload: {
        readonly issueId: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>
    query
    (
    "GetIssueAssignee", { issueId: number & Brand<"IssueId">issueId }, { serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );
  2. Set up the registry

    getRouter runs once per request on the server and once in the browser. makeRegistry() makes a request registry on the server and the browser’s registry in the browser, and Loader.make puts the verbs on the router context.

    setupRouterAtoms streams the server’s registry into the page once the loaders have run, and hydrates the browser’s from it. The root route turns on WaterfallDetection and catches RpcClientError and Unauthorized once, for every page.

    TanStack Start
    src/router.tsx
    export function 
    function getRouter(): RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
    getRouter
    () {
    // A request registry on the server, the browser's registry in the browser. const const registry: AtomRegistryregistry =
    function makeRegistry(options?: {
        readonly defaultIdleTTL?: number | undefined;
        readonly initialValues?: InitialValues | undefined;
        readonly seed?: Seed | undefined;
    }): AtomRegistry
    makeRegistry
    ();
    const
    const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
    router
    =
    createRouter<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>(options: RouterConstructorOptions<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>): RouterCore<...>
    createRouter
    ({
    routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined> | undefined
    routeTree
    ,
    context: {
        atoms: Loader.Loader;
    }
    context
    : { atoms: Loader.Loaderatoms: import LoaderLoader.const make: (registry: AtomRegistry, options?: Loader.MakeOptions) => Loader.Loadermake(const registry: AtomRegistryregistry) },
    defaultPreload?: false | "intent" | "viewport" | "render" | undefineddefaultPreload: "intent", });
    setupRouterAtoms<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>(options: RouterAtomsOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>>): void
    setupRouterAtoms
    ({
    RouterAtomsOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, "never", false, RouterHistory, Record<...>>>.router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
    router
    , RouterAtomsOptions<TRouter extends AnyRouter>.registry: AtomRegistryregistry });
    return
    const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
    router
    ;
    }
    src/routes/__root.tsx
    export const 
    const Route: RootRoute<Register, undefined, {
        atoms: Loader.Loader;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
    Route
    =
    createRootRouteWithContext<{
        atoms: Loader.Loader;
    }>(): <TRegister, TRouteContextFn, TBeforeLoadFn, TSearchValidator, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>(options?: RootRouteOptions<TRegister, TSearchValidator, {
        atoms: Loader.Loader;
    }, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares, undefined> | undefined) => RootRoute<...>
    createRootRouteWithContext
    <{ atoms: Loader.Loaderatoms: import LoaderLoader.Loader }>()({
    component?: RouteComponent | undefinedcomponent: () => ( // In development, a component that starts an atom no loader started throws. <
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    mode: Modemode="throw">
    <const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> <const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary> <const Outlet: React.MemoExoticComponent<() => import("react").JSX.Element | null>Outlet /> </const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary> </const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> </
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    >
    ), });
  3. Start them in the route loader

    The router runs the loader as soon as a navigation starts, and with defaultPreload: "intent", when a link is hovered. Every atom the page needs starts there, at the same time, before anything renders.

    prefetch starts an atom without waiting, and ensure waits for one the page can’t lay out without. The loader returns the handles they return, for the page to pass down. Results still loading cross to the browser as promises, so nothing is fetched twice.

    Loader verbs
    src/routes/issues.$id.tsx
    export const 
    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
    =
    createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
        atoms: Loader;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
    createFileRoute
    ("/issues/$id")({
    params?: {
        parse?: ParseParamsFn<"/issues/$id", {
            readonly id: number & Brand<"IssueId">;
        }> | undefined;
        priority?: number;
        stringify?: StringifyParamsFn<"/issues/$id", {
            readonly id: number & Brand<"IssueId">;
        }> | undefined;
    } | undefined
    params
    : {
    parse?: ParseParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined
    parse
    : (
    params: {
        id: string;
    }
    params
    ) => import SchemaSchema.
    function decodeUnknownSync<Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>>(schema: Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
        readonly id: number & Brand<"IssueId">;
    }
    decodeUnknownSync
    (
    const IssueParams: Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>
    IssueParams
    )(
    params: {
        id: string;
    }
    params
    ),
    stringify?: StringifyParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined
    stringify
    : import SchemaSchema.
    const encodeSync: <Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>>(schema: Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>, options?: ParseOptions) => (input: {
        readonly id: number & Brand<"IssueId">;
    }, options?: ParseOptions) => {
        readonly id: string;
    }
    encodeSync
    (
    const IssueParams: Schema.Struct<{
        readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
    }>
    IssueParams
    ),
    },
    FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader; }, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
        atoms: Loader;
    }, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
    loader
    : async ({
    context: {
        atoms: Loader;
    }
    context
    ,
    params: {
        readonly id: number & Brand<"IssueId">;
    }
    params
    }) => ({
    // Starts the assignee; it streams in while the page renders. assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:
    context: {
        atoms: Loader;
    }
    context
    .atoms: Loaderatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
    params: {
        readonly id: number & Brand<"IssueId">;
    }
    params
    .id: number & Brand<"IssueId">id),
    // Waits for the issue, so the page renders with it. issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await
    context: {
        atoms: Loader;
    }
    context
    .atoms: Loaderatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
    params: {
        readonly id: number & Brand<"IssueId">;
    }
    params
    .id: number & Brand<"IssueId">id),
    }), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: () => ( <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <function IssuePage(): React.JSX.ElementIssuePage /> </const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> ), });
  4. Read them in components

    A component takes the handle of the atom it reads, not an id, and useAtomResult reads that atom from the registry: it suspends until the result settles, and never starts a request of its own. Wrapped by the issue boundary, every failure goes to the boundary that claims its tag, so none is left in the type and the result is a success.

    A result still loading suspends only its own subtree, so the assignee streams in under its Suspense while the issue shows.

    Reading atoms
    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>; }
  5. Catch failures by tag

    A boundary names what its errors come from, here the RPC client. Its catchTags keys are that client’s error tags, and each handler gets the error with that tag. UI code never imports an error class.

    The idea comes from result-rpc.

    Error boundaries
    src/boundaries.tsx
    // Under the Auth boundary: it claims IssueNotFound, and Unauthorized and
    // RpcClientError still go up the chain.
    export const [const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary, const useIssueBoundary: UseErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">useIssueBoundary] = createErrorBoundary<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">(options: ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">): ErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">createErrorBoundary({
    	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "Issue",
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.errors: typeof IssuesClienterrors: class IssuesClientIssuesClient,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.parent?: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError"> | undefinedparent: const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | EmptyTitle | EmptyBody | EmptyComment | Unauthorized, "IssueNotFound">catchTags: {
    		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror }) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
    	},
    });

A page says what it needs before it renders. The atoms are made once from the RPC client, the server component starts every request as the page renders, client components read the results as they arrive, and boundaries catch the failures by tag.

  1. Make atoms from the RPC client

    AtomRpc.Service turns the RPC contract into atoms: IssuesClient.query("GetIssue", { id }) is an atom of that call’s AsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.

    #rpc-transport picks where requests go: server components call the RPC handler in the same process, and client components POST /rpc.

    Quick start
    src/issues-client.ts
    export class class IssuesClientIssuesClient extends import AtomRpcAtomRpc.
    const Service: <IssuesClient>() => <Id, Rpcs, ER, RM>(id: Id, options: {
        readonly group: RpcGroup<Rpcs>;
        readonly protocol: Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never> | ((get: AtomContext) => Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never>);
        readonly spanPrefix?: string | undefined;
        readonly spanAttributes?: Record<string, unknown> | undefined;
        readonly generateRequestId?: (() => RequestId) | undefined;
        readonly disableTracing?: boolean | undefined;
        readonly makeEffect?: Effect<...> | undefined;
        readonly runtime?: RuntimeFactory | undefined;
    }) => AtomRpc.AtomRpcClient<...>
    Service
    <class IssuesClientIssuesClient>()(
    "IssuesClient", {
    group: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    group
    :
    const IssueRpcs: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    IssueRpcs
    ,
    protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol: import RpcClientRpcClient.
    const layerProtocolHttp: (options: {
        readonly url: string;
        readonly transformClient?: <E, R>(client: HttpClient.With<E, R>) => HttpClient.With<E, R>;
        readonly batch?: {
            readonly window: Input;
            readonly maxSize?: number | undefined;
        } | undefined;
    }) => Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>
    layerProtocolHttp
    ({ url: stringurl: const rpcUrl: "/rpc"rpcUrl }).Pipeable.pipe<Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, Layer.Layer<RpcClient.Protocol, never, never>>(this: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, ab: (_: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>) => Layer.Layer<RpcClient.Protocol, never, never>): Layer.Layer<...> (+21 overloads)pipe(
    import LayerLayer.const provide: <[Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]>(that: [Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, RpcSerialization.RpcSerialization | HttpClient>> (+3 overloads)provide([const RpcHttpClient: Layer.Layer<HttpClient, never, never>RpcHttpClient, import RpcSerializationRpcSerialization.const layerJson: Layer.Layer<RpcSerialization.RpcSerialization, never, never>layerJson]), ), }, ) {}
    src/issues.ts
    // A serialization key lets the server send the result to the browser.
    export const issue = (id: number & Brand<"IssueId">id: type IssueId = number & Brand<"IssueId">IssueId) =>
    const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssue">(tag: "GetIssue", payload: {
        readonly id: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    query
    ("GetIssue", { id: number & Brand<"IssueId">id }, { serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` });
    export const const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId: type IssueId = number & Brand<"IssueId">IssueId) => class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssueAssignee">(tag: "GetIssueAssignee", payload: {
        readonly issueId: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>
    query
    (
    "GetIssueAssignee", { issueId: number & Brand<"IssueId">issueId }, { serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );
  2. Set up the registry

    Server components share one request registry, which the verbs make through React’s cache and dispose when the render ends. The root layout holds the browser’s registry in RegistryProvider, turns on WaterfallDetection, and catches RpcClientError and Unauthorized once, for every page.

    Streaming and hydration
    src/app/layout.tsx
    // The verbs make each request's registry, so the server needs no setup.
    export default function 
    function RootLayout({ children }: {
        children: ReactNode;
    }): React.JSX.Element
    RootLayout
    ({ children: ReactNodechildren }: { children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedReactNode }) {
    return ( <React.JSX.IntrinsicElements.html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html React.HTMLAttributes<HTMLHtmlElement>.lang?: string | undefinedlang="en"> <React.JSX.IntrinsicElements.body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> {/* The browser's registry, which each page's HydrationBoundary fills. */} <
    const RegistryProvider: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly defaultIdleTTL?: number | undefined;
    }) => React.ReactNode
    RegistryProvider
    >
    {/* In development, a component that starts an atom no server component started throws. */} <
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    mode: Modemode="throw">
    <const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> <const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary>{children: ReactNodechildren}</const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary> </const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> </
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    >
    </
    const RegistryProvider: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly defaultIdleTTL?: number | undefined;
    }) => React.ReactNode
    RegistryProvider
    >
    </React.JSX.IntrinsicElements.body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> </React.JSX.IntrinsicElements.html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html> ); }
  3. Start them in a server component

    A server component runs as soon as the App Router renders the page. Every atom the page needs starts there, at the same time, before client components render.

    prefetch starts an atom without waiting, and ensure waits for one the page can’t lay out without. Each returns a handle, which the page passes to the client component that reads the atom. In a server component, HydrationBoundary takes the request’s registry and sends it to the browser’s, with the results still loading as promises, so nothing is fetched twice.

    Loader verbs
    src/app/issues/[id]/page.tsx
    export default async function 
    function Page(props: {
        params: Promise<{
            id: string;
        }>;
    }): Promise<React.JSX.Element>
    Page
    (
    props: {
        params: Promise<{
            id: string;
        }>;
    }
    props
    : {
    params: Promise<{
        id: string;
    }>
    params
    : interface Promise<T>Promise<{ id: stringid: string }> }) {
    const const id: number & Brand<"IssueId">id = import SchemaSchema.const decodeSync: <Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>>(schema: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>, options?: ParseOptions) => (input: string, options?: ParseOptions) => number & Brand<"IssueId">decodeSync(const IssueIdFromString: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>IssueIdFromString)((await
    props: {
        params: Promise<{
            id: string;
        }>;
    }
    props
    .
    params: Promise<{
        id: string;
    }>
    params
    ).id: stringid);
    // Starts the assignee; it streams in while the page renders. const const assigneeHandle: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle = prefetch<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">): Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id); // Waits for the issue, so the page renders with it. const const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle = await ensure<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">): Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id); return ( // The request registry's results go to the browser, still-loading ones // as promises. <const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary registry: AtomRegistryregistry={function getRegistry(): AtomRegistrygetRegistry()}> <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <
    function IssuePage(props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }): React.JSX.Element
    IssuePage
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={const assigneeHandle: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle} issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue={const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle} />
    </const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> </const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary> ); }
  4. Read them in client components

    Client components live in their own files, marked "use client".

    A component takes the handle of the atom it reads, not an id, and useAtomResult reads that atom from the registry: it suspends until the result settles, and never starts a request of its own. Wrapped by the issue boundary, every failure goes to the boundary that claims its tag, so none is left in the type and the result is a success.

    A result still loading suspends only its own subtree, so the assignee streams in under its Suspense while the issue shows.

    Reading atoms
    src/components/issue-page.tsx
    "use client";
    export function 
    function IssuePage(props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }): React.JSX.Element
    IssuePage
    (
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    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>; issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue>; }) { // 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,
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    props
    .issue: Handle<(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={
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    props
    .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 server component passed, 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>; }
  5. Catch failures by tag

    A boundary names what its errors come from, here the RPC client. Its catchTags keys are that client’s error tags, and each handler gets the error with that tag. UI code never imports an error class.

    Boundaries are client components too. The server page renders the issue boundary around IssuePage.

    The idea comes from result-rpc.

    Error boundaries
    src/boundaries.tsx
    "use client";
    // Under the Auth boundary: it claims IssueNotFound, and Unauthorized and
    // RpcClientError still go up the chain.
    export const [const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary, const useIssueBoundary: UseErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">useIssueBoundary] = createErrorBoundary<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">(options: ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">): ErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">createErrorBoundary({
    	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "Issue",
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.errors: typeof IssuesClienterrors: class IssuesClientIssuesClient,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.parent?: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError"> | undefinedparent: const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | EmptyTitle | EmptyBody | EmptyComment | Unauthorized, "IssueNotFound">catchTags: {
    		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror }) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
    	},
    });

A page says what it needs before it renders. The atoms are made once from the RPC client, the route’s loader starts every request as the navigation begins, components read the results as they arrive, and boundaries catch the failures by tag.

  1. Make atoms from the RPC client

    AtomRpc.Service turns the RPC contract into atoms: IssuesClient.query("GetIssue", { id }) is an atom of that call’s AsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.

    #rpc-transport picks where requests go: on the server they call the RPC handler in the same process, and in the browser they POST /rpc, a resource route.

    Quick start
    app/issues-client.ts
    export class class IssuesClientIssuesClient extends import AtomRpcAtomRpc.
    const Service: <IssuesClient>() => <Id, Rpcs, ER, RM>(id: Id, options: {
        readonly group: RpcGroup<Rpcs>;
        readonly protocol: Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never> | ((get: AtomContext) => Layer.Layer<Exclude<NoInfer<RM>, Scope>, ER, never>);
        readonly spanPrefix?: string | undefined;
        readonly spanAttributes?: Record<string, unknown> | undefined;
        readonly generateRequestId?: (() => RequestId) | undefined;
        readonly disableTracing?: boolean | undefined;
        readonly makeEffect?: Effect<...> | undefined;
        readonly runtime?: RuntimeFactory | undefined;
    }) => AtomRpc.AtomRpcClient<...>
    Service
    <class IssuesClientIssuesClient>()(
    "IssuesClient", {
    group: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    group
    :
    const IssueRpcs: RpcGroup<Rpc<"ListIssues", Struct<{
        cursor: Int;
        sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>;
        status: withDecodingDefault<Literals<readonly ["all", "open", "in_progress", "closed"]>, never>;
    }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | Rpc<"SearchIssues", Struct<{
        query: String;
    }>, $Array<typeof Issue>, Never, typeof Latency | typeof Authorization, never> | ... 7 more ... | Rpc<...>>
    IssueRpcs
    ,
    protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol: import RpcClientRpcClient.
    const layerProtocolHttp: (options: {
        readonly url: string;
        readonly transformClient?: <E, R>(client: HttpClient.With<E, R>) => HttpClient.With<E, R>;
        readonly batch?: {
            readonly window: Input;
            readonly maxSize?: number | undefined;
        } | undefined;
    }) => Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>
    layerProtocolHttp
    ({ url: stringurl: const rpcUrl: "http://in-process/rpc"rpcUrl }).Pipeable.pipe<Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, Layer.Layer<RpcClient.Protocol, never, never>>(this: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>, ab: (_: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization | HttpClient>) => Layer.Layer<RpcClient.Protocol, never, never>): Layer.Layer<...> (+21 overloads)pipe(
    import LayerLayer.const provide: <[Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]>(that: [Layer.Layer<HttpClient, never, never>, Layer.Layer<RpcSerialization.RpcSerialization, never, never>]) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, RpcSerialization.RpcSerialization | HttpClient>> (+3 overloads)provide([const RpcHttpClient: Layer.Layer<HttpClient, never, never>RpcHttpClient, import RpcSerializationRpcSerialization.const layerJson: Layer.Layer<RpcSerialization.RpcSerialization, never, never>layerJson]), ), }, ) {}
    app/issues.ts
    // A serialization key lets the server send the result to the browser.
    export const issue = (id: number & Brand<"IssueId">id: type IssueId = number & Brand<"IssueId">IssueId) =>
    const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssue">(tag: "GetIssue", payload: {
        readonly id: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
    query
    ("GetIssue", { id: number & Brand<"IssueId">id }, { serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` });
    export const const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId: type IssueId = number & Brand<"IssueId">IssueId) => class IssuesClientIssuesClient.
    AtomRpcClient<IssuesClient, "IssuesClient", Rpc<"ListIssues", Struct<{ cursor: Int; sort: withDecodingDefault<Literals<readonly ["newest", "oldest"]>, never>; status: withDecodingDefault<...>; }>, typeof IssuePage, Never, typeof Latency | typeof Authorization, never> | ... 8 more ... | Rpc<...>>.query: <"GetIssueAssignee">(tag: "GetIssueAssignee", payload: {
        readonly issueId: number & Brand<"IssueId">;
    }, options?: {
        readonly headers?: Input | undefined;
        readonly reactivityKeys?: KeysInput | undefined;
        readonly timeToLive?: Input | undefined;
        readonly serializationKey?: string | undefined;
    }) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>
    query
    (
    "GetIssueAssignee", { issueId: number & Brand<"IssueId">issueId }, { serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );
  2. Set up the registry

    atomsMiddleware gives each server request its own registry, under a router context every loader reads. In the browser, the root’s RegistryProvider holds the registry each route’s HydrationBoundary fills, and clientAtomsMiddleware hands it to client loaders. The root also turns on WaterfallDetection and catches RpcClientError and Unauthorized once, for every page.

    Streaming and hydration
    app/atoms.ts
    import type { RequestAtoms } from "raf-effect-atom/react-router";
    import { function createContext<T>(defaultValue?: T): RouterContext<T>createContext } from "react-router";
    
    // Each server request's registry, set by the root's middleware.
    export const const atomsContext: RouterContext<RequestAtoms>atomsContext = createContext<RequestAtoms>(defaultValue?: RequestAtoms | undefined): RouterContext<RequestAtoms>createContext<RequestAtoms>();
    app/root.tsx
    // Gives each server request its own registry, which every loader reads.
    export const 
    const middleware: CreateServerMiddlewareFunction<Info & {
        module: typeof import("./root");
        matches: Matches;
    }>[]
    middleware
    : interface Array<T>Array<Route.
    type Route.MiddlewareFunction = (args: ServerDataFunctionArgs<{
        id?: undefined;
    } | {
        id: string;
    }>, next: MiddlewareNextFunction<Response>) => MaybePromise<void | Response>
    MiddlewareFunction
    > = [
    function atomsMiddleware(context: AtomsContext, options?: AtomsMiddlewareOptions): <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>atomsMiddleware(const atomsContext: RouterContext<RequestAtoms>atomsContext), ]; // Gives client loaders the browser's registry, under the same context. export const
    const clientMiddleware: CreateClientMiddlewareFunction<Info & {
        module: typeof import("./root");
        matches: Matches;
    }>[]
    clientMiddleware
    : interface Array<T>Array<Route.
    type Route.ClientMiddlewareFunction = (args: ClientDataFunctionArgs<{
        id?: undefined;
    } | {
        id: string;
    }>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<void | Record<string, DataStrategyResult>>
    ClientMiddlewareFunction
    > = [
    function clientAtomsMiddleware(context: AtomsContext): <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>clientAtomsMiddleware(const atomsContext: RouterContext<RequestAtoms>atomsContext), ]; export function
    function Layout({ children }: {
        children: ReactNode;
    }): React.JSX.Element
    Layout
    ({ children: ReactNodechildren }: { children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedReactNode }) {
    return ( <React.JSX.IntrinsicElements.html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html React.HTMLAttributes<HTMLHtmlElement>.lang?: string | undefinedlang="en"> <React.JSX.IntrinsicElements.head: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>head> <function Meta(): React.JSX.ElementMeta /> <function Links({ nonce, crossOrigin }: LinksProps): React.JSX.ElementLinks /> </React.JSX.IntrinsicElements.head: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>head> <React.JSX.IntrinsicElements.body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> {/* The browser's registry, which each route's HydrationBoundary fills. */} <
    const RegistryProvider: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly defaultIdleTTL?: number | undefined;
    }) => React.ReactNode
    RegistryProvider
    >
    {/* In development, a component that starts an atom no loader started throws. */} <
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    mode: Modemode="throw">{children: ReactNodechildren}</
    const WaterfallDetection: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly mode: WaterfallMode;
    }) => React.ReactNode
    WaterfallDetection
    >
    </
    const RegistryProvider: (props: {
        readonly children?: React.ReactNode | undefined;
        readonly defaultIdleTTL?: number | undefined;
    }) => React.ReactNode
    RegistryProvider
    >
    <function Scripts(scriptProps: ScriptsProps): React.JSX.Element | nullScripts /> </React.JSX.IntrinsicElements.body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> </React.JSX.IntrinsicElements.html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html> ); } export default function function App(): React.JSX.ElementApp() { return ( <const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> <const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary> <function Outlet(props: OutletProps): React.ReactElement | nullOutlet /> </const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary> </const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> ); }
  3. Start them in the route loader

    React Router runs the loader as soon as a navigation starts: on the server for the first page, and in the browser after that, as the route’s clientLoader. Every atom the page needs starts there, at the same time, before anything renders.

    prefetch starts an atom without waiting, and ensure waits for one the page can’t lay out without. The loader returns the handles they return with atoms.dehydrate(), React Router streams the results still loading, and HydrationBoundary writes them into the browser’s registry, so nothing is fetched twice.

    Loader verbs
    app/routes/issues.$id.tsx
    async function 
    function load({ context, params }: Route.LoaderArgs): Promise<{
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    }>
    load
    ({ context: Readonly<RouterContextProvider>context,
    params: {
        id: string;
    }
    params
    }: Route.
    type Route.LoaderArgs = {
        request: Request;
        url: URL;
        params: {
            id: string;
        };
        pattern: string;
        context: Readonly<RouterContextProvider>;
    }
    LoaderArgs
    ) {
    const const id: number & Brand<"IssueId">id = import SchemaSchema.const decodeSync: <Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>>(schema: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>, options?: ParseOptions) => (input: string, options?: ParseOptions) => number & Brand<"IssueId">decodeSync(const IssueIdFromString: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>IssueIdFromString)(
    params: {
        id: string;
    }
    params
    .id: stringid);
    const const atoms: RequestAtomsatoms = context: Readonly<RouterContextProvider>context.get<RequestAtoms>(context: RouterContext<RequestAtoms>): RequestAtomsget(const atomsContext: RouterContext<RequestAtoms>atomsContext); const
    const handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    }
    handles
    = {
    // Starts the assignee; it streams in while the page renders. assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: const atoms: RequestAtomsatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id), // Waits for the issue, so the page renders with it. issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await const atoms: RequestAtomsatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id), }; // The registry's results go to the browser, still-loading ones as promises. return { atoms: DehydratedAtom[]atoms: const atoms: RequestAtomsatoms.RequestAtoms.dehydrate: () => Array<DehydratedAtom>dehydrate(),
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    }
    handles
    };
    } // The first page loads on the server; after it, the same function runs in // the browser, where `ensure` finds what the registry already holds. export const
    const loader: ({ context, params }: Route.LoaderArgs) => Promise<{
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    }>
    loader
    =
    function load({ context, params }: Route.LoaderArgs): Promise<{
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    }>
    load
    ;
    export const
    const clientLoader: ({ context, params }: Route.LoaderArgs) => Promise<{
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    }>
    clientLoader
    =
    function load({ context, params }: Route.LoaderArgs): Promise<{
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    }>
    load
    ;
    export default function function Page({ loaderData }: Route.ComponentProps): React.JSX.ElementPage({
    loaderData: {
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    } | {
        atoms: {
            readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
        }[];
        handles: {
            ...;
        };
    }
    loaderData
    }: Route.
    type Route.ComponentProps = {
        params: {
            id: string;
        };
        matches: [{
            id: "root";
            params: Record<string, string | undefined>;
            pathname: string;
            loaderData: undefined;
            handle: unknown;
        }, {
            id: "routes/issues.$id";
            params: Record<string, string | undefined>;
            pathname: string;
            loaderData: {
                atoms: DehydratedAtom[];
                handles: {
                    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
                    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
                };
            } | {
                ...;
            };
            handle: unknown;
        }, ...({
            ...;
        } | undefined)[]];
    } & {
        ...;
    }
    ComponentProps
    ) {
    return ( <const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary state?: Iterable<DehydratedAtom> | undefinedstate={
    loaderData: {
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    } | {
        atoms: {
            readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
        }[];
        handles: {
            ...;
        };
    }
    loaderData
    .
    atoms: DehydratedAtom[] | {
        readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
    }[]
    atoms
    }>
    <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <
    function IssuePage(props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }): React.JSX.Element
    IssuePage
    {...
    loaderData: {
        atoms: DehydratedAtom[];
        handles: {
            assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
            issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
        };
    } | {
        atoms: {
            readonly "~effect/reactivity/Hydration/DehydratedAtom": true;
        }[];
        handles: {
            ...;
        };
    }
    loaderData
    .
    handles: {
        assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
        issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
    } | {
        assignee: {
            readonly args: [issueId: number & Brand<"IssueId">];
            readonly inBrowser?: true | undefined;
            readonly key: HandleKey<...> | undefined;
        };
        issue: {
            ...;
        };
    }
    handles
    } />
    </const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> </const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary> ); }
  4. Read them in components

    A component takes the handle of the atom it reads, not an id, and useAtomResult reads that atom from the registry: it suspends until the result settles, and never starts a request of its own. Wrapped by the issue boundary, every failure goes to the boundary that claims its tag, so none is left in the type and the result is a success.

    A result still loading suspends only its own subtree, so the assignee streams in under its Suspense while the issue shows.

    Reading atoms
    app/routes/issues.$id.tsx
    function 
    function IssuePage(props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }): React.JSX.Element
    IssuePage
    (
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    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>; issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue>; }) { // 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,
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    props
    .issue: Handle<(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={
    props: {
        assignee: Handle<typeof assignee>;
        issue: Handle<typeof issue>;
    }
    props
    .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>; }
  5. Catch failures by tag

    A boundary names what its errors come from, here the RPC client. Its catchTags keys are that client’s error tags, and each handler gets the error with that tag. UI code never imports an error class.

    The idea comes from result-rpc.

    Error boundaries
    app/boundaries.tsx
    // Under the Auth boundary: it claims IssueNotFound, and Unauthorized and
    // RpcClientError still go up the chain.
    export const [const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary, const useIssueBoundary: UseErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">useIssueBoundary] = createErrorBoundary<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">(options: ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">): ErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">createErrorBoundary({
    	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "Issue",
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.errors: typeof IssuesClienterrors: class IssuesClientIssuesClient,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.parent?: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError"> | undefinedparent: const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary,
    	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | EmptyTitle | EmptyBody | EmptyComment | Unauthorized, "IssueNotFound">catchTags: {
    		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror }) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
    	},
    });

Two packages, each on its own

Use the boundaries without the loaders, or the loaders without the boundaries. Together, a failure from the server is caught in the browser by the boundary that claims its tag.

  • effect-atom-boundaryError boundaries typed by an atom's error. Catch failures by _tag, with a handler per tag typed by the error with that tag. Works with plain @effect/atom-react.$ pnpm add effect-atom-boundaryRead the guide
  • raf-effect-atomStart atoms from route loaders with prefetch, ensure or fetch, stream them into the browser's registry with their typed failures, and read them under Suspense.$ pnpm add raf-effect-atomRead the guide

Wrong code fails loudly

Coding agents write what compiles and runs. Here, the usual mistakes don’t: a fetch in a component throws in development, and a misspelled tag, or a match that forgets one, is a type error. The agent is corrected by the tools before a reviewer sees the code.

llms.txt starts with the rules, so an agent reads what to write before it writes it.

Rules for coding agents
  1. Fetches in a componentDoesn't compile

    // Reads take the handle a loader returned. An id would start the request
    // in render, so it isn't one.
    function 
    function Assignee(props: {
        assignee: Handle<typeof assignee>;
    }): React.JSX.Element
    Assignee
    (
    props: {
        assignee: Handle<typeof assignee>;
    }
    props
    : { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: interface Handle<S>Handle<typeof const assignee: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) {
    const const result: AsyncResult.Success<User, never>result = useIssueBoundary<SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>): AsyncResult.Success<User, never>useIssueBoundary(useAtomResult<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(issueId: number & Brand<"IssueId">) => Atom<AsyncResult.AsyncResult<User, unknown>>>>): SettledResult<...>useAtomResult(const assignee: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
    props: {
        assignee: Handle<typeof assignee>;
    }
    props
    .assignee: Handle<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee));
    return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const result: AsyncResult.Success<User, never>result.Success<User, never>.value: Uservalue.name: stringname}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; } function
    function IssuePage(props: {
        issueId: IssueId;
    }): React.JSX.Element
    IssuePage
    (
    props: {
        issueId: IssueId;
    }
    props
    : { issueId: number & Brand<"IssueId">issueId: type IssueId = number & Brand<"IssueId">IssueId }) {
    return <
    function Assignee(props: {
        assignee: Handle<typeof assignee>;
    }): React.JSX.Element
    Assignee
    assignee={
    props: {
        issueId: IssueId;
    }
    props
    .issueId: number & Brand<"IssueId">issueId} />;
    Type 'Number & Brand<"IssueId">' is missing the following properties from type 'Handle<(issueId: number & Brand<"IssueId">) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>': args, key
    }
  2. Misspells a tagDoesn't compile

    export const [const NotFoundBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">NotFoundBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized" | "RpcClientError", never>(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized" | "RpcClientError", never>): ErrorBoundary<...>createErrorBoundary({
    	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "NotFound",
    	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | ... 1 more ... | "RpcClientError", never>.errors: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
    	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | ... 1 more ... | "RpcClientError", never>.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "IssueNotFound" | "Unauthorized" | "RpcClientError">catchTags: {
    		IssueNotFund: () => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Not found</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
    Object literal may only specify known properties, but 'IssueNotFund' does not exist in type 'CatchTags<RpcClientError | IssueNotFound | Unauthorized, "RpcClientError" | "IssueNotFound" | "Unauthorized">'. Did you mean to write 'IssueNotFound'?
    }, });
  3. Forgets a tagDoesn't compile

    // The app boundary claims RpcClientError: IssueNotFound and Unauthorized
    // are left.
    function 
    function IssueTitle(props: {
        issue: Handle<typeof issue>;
    }): any
    IssueTitle
    (
    props: {
        issue: Handle<typeof issue>;
    }
    props
    : { issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) {
    const const result: AsyncResult.Success<Issue, IssueNotFound | Unauthorized> | AsyncResult.Failure<Issue, IssueNotFound | Unauthorized>result = useAppBoundary<SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>): AsyncResult.Success<Issue, IssueNotFound | Unauthorized> | AsyncResult.Failure<Issue, IssueNotFound | Unauthorized>useAppBoundary(useAtomResult<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(id: number & Brand<"IssueId">) => Atom<AsyncResult.AsyncResult<Issue, unknown>>>>): SettledResult<Issue, RpcClientError | ... 1 more ... | Unauthorized>useAtomResult(const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
    props: {
        issue: Handle<typeof issue>;
    }
    props
    .issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue));
    if (import AsyncResultAsyncResult.const isSuccess: <Issue, IssueNotFound | Unauthorized>(result: AsyncResult.AsyncResult<Issue, IssueNotFound | Unauthorized>) => result is AsyncResult.Success<Issue, IssueNotFound | Unauthorized>isSuccess(const result: AsyncResult.Success<Issue, IssueNotFound | Unauthorized> | AsyncResult.Failure<Issue, IssueNotFound | Unauthorized>result)) { return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const result: AsyncResult.Success<Issue, IssueNotFound | Unauthorized>result.Success<Issue, IssueNotFound | Unauthorized>.value: Issuevalue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; } return import OptionOption.
    const match: <IssueNotFound | Unauthorized, React.JSX.Element, any>(self: Option.Option<IssueNotFound | Unauthorized>, options: {
        readonly onNone: LazyArg<React.JSX.Element>;
        readonly onSome: (a: IssueNotFound | Unauthorized) => any;
    }) => any (+1 overload)
    match
    (import AsyncResultAsyncResult.const error: <Issue, IssueNotFound | Unauthorized>(self: AsyncResult.AsyncResult<Issue, IssueNotFound | Unauthorized>) => Option.Option<IssueNotFound | Unauthorized>error(const result: AsyncResult.Failure<Issue, IssueNotFound | Unauthorized>result), {
    onNone: LazyArg<React.JSX.Element>onNone: () => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>The server died.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>, onSome: (a: IssueNotFound | Unauthorized) => anyonSome: (error: IssueNotFound | Unauthorizederror) => import MatchMatch.const valueTags: <IssueNotFound | Unauthorized, ValueTagHandlers<IssueNotFound | Unauthorized> & {}>(input: IssueNotFound | Unauthorized, fields: ValueTagHandlers<IssueNotFound | Unauthorized> & {}) => any (+1 overload)valueTags(error: IssueNotFound | Unauthorizederror, {
    Argument of type '{ IssueNotFound: (notFound: IssueNotFound) => Element; }' is not assignable to parameter of type 'ValueTagHandlers<IssueNotFound | Unauthorized> & {}'. Property 'Unauthorized' is missing in type '{ IssueNotFound: (notFound: IssueNotFound) => Element; }' but required in type 'ValueTagHandlers<IssueNotFound | Unauthorized>'.
    type IssueNotFound: (_: IssueNotFound) => anyIssueNotFound: (notFound: IssueNotFoundnotFound) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{notFound: IssueNotFoundnotFound.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>, }), }); }

What you get