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.
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)); 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>;
}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>
);
}"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)); 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>;
}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)); 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.
Make atoms from the RPC client
Quick startAtomRpc.Serviceturns the RPC contract into atoms:IssuesClient.query("GetIssue", { id })is an atom of that call’sAsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.#rpc-transportpicks where requests go: during server rendering they call the RPC handler in the same process, and in the browser theyPOST /rpc, a server route.src/issues-client.tsexport classclass IssuesClientIssuesClient extendsimport AtomRpcAtomRpc.Service<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<...>class IssuesClientIssuesClient>()( "IssuesClient", {group: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<...>>IssueRpcs,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<...>>protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol:import RpcClientRpcClient.layerProtocolHttp({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>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) =>class IssuesClientIssuesClient.query("GetIssue", {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>>id: number & Brand<"IssueId">id }, {serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` }); export constconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId:type IssueId = number & Brand<"IssueId">IssueId) =>class IssuesClientIssuesClient.query( "GetIssueAssignee", {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>>issueId: number & Brand<"IssueId">issueId }, {serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );Set up the registry
TanStack StartgetRouterruns 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, andLoader.makeputs the verbs on the router context.setupRouterAtomsstreams the server’s registry into the page once the loaders have run, and hydrates the browser’s from it. The root route turns onWaterfallDetectionand catchesRpcClientErrorandUnauthorizedonce, for every page.src/router.tsxexport functiongetRouter() { // A request registry on the server, the browser's registry in the browser. constfunction 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>>const registry: AtomRegistryregistry =makeRegistry(); constfunction makeRegistry(options?: { readonly defaultIdleTTL?: number | undefined; readonly initialValues?: InitialValues | undefined; readonly seed?: Seed | undefined; }): AtomRegistryrouter =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>>createRouter({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<...>routeTree,routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined> | undefinedcontext: {context: { atoms: Loader.Loader; }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({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<...>>>): voidrouter,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>>RouterAtomsOptions<TRouter extends AnyRouter>.registry: AtomRegistryregistry }); returnrouter; }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>>src/routes/__root.tsxexport constRoute =const Route: RootRoute<Register, undefined, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>createRootRouteWithContext<{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<...>atoms: Loader.Loaderatoms:import LoaderLoader.Loader }>()({component?: RouteComponent | undefinedcomponent: () => ( // In development, a component that starts an atom no loader started throws. <WaterfallDetectionconst WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodemode: 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> </WaterfallDetection> ), });const WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodeStart them in the route loader
Loader verbsThe 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.prefetchstarts an atom without waiting, andensurewaits 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.src/routes/issues.$id.tsxexport constRoute =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>createFileRoute("/issues/$id")({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<...>params: {params?: { parse?: ParseParamsFn<"/issues/$id", { readonly id: number & Brand<"IssueId">; }> | undefined; priority?: number; stringify?: StringifyParamsFn<"/issues/$id", { readonly id: number & Brand<"IssueId">; }> | undefined; } | undefinedparse: (parse?: ParseParamsFn<"/issues/$id", { readonly id: number & Brand<"IssueId">; }> | undefinedparams) =>params: { id: string; }import SchemaSchema.decodeUnknownSync(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">; }IssueParams)(const IssueParams: Schema.Struct<{ readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>; }>params),params: { id: string; }stringify:stringify?: StringifyParamsFn<"/issues/$id", { readonly id: number & Brand<"IssueId">; }> | undefinedimport SchemaSchema.encodeSync(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; }IssueParams), },const IssueParams: Schema.Struct<{ readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>; }>loader: async ({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<...>> | undefinedcontext,context: { atoms: Loader; }params }) => ({ // Starts the assignee; it streams in while the page renders.params: { readonly id: number & Brand<"IssueId">; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:context.context: { atoms: Loader; }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.params: { readonly id: number & Brand<"IssueId">; }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: awaitcontext.context: { atoms: Loader; }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.params: { readonly id: number & Brand<"IssueId">; }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> ), });Read them in components
Reading atomsA component takes the handle of the atom it reads, not an id, and
useAtomResultreads 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
Suspensewhile the issue shows.src/routes/issues.$id.tsxfunctionfunction IssuePage(): React.JSX.ElementIssuePage() { constdata =const data: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>; issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>; }Route.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>useLoaderData(); // IssueNotFound, Unauthorized and RpcClientError go to their boundaries, // so no failure is left. const result =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) => { ...; }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,data.const data: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>; issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>; }issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue)); 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>SuspenseReact.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>}> <Assigneefunction Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementassignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={data.const data: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>; issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>; }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. functionAssignee(function Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) { constconst 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.props: { assignee: Handle<typeof assignee>; }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>; }Catch failures by tag
Error boundariesA boundary names what its errors come from, here the RPC client. Its
catchTagskeys 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.
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.
Make atoms from the RPC client
Quick startAtomRpc.Serviceturns the RPC contract into atoms:IssuesClient.query("GetIssue", { id })is an atom of that call’sAsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.#rpc-transportpicks where requests go: server components call the RPC handler in the same process, and client componentsPOST /rpc.src/issues-client.tsexport classclass IssuesClientIssuesClient extendsimport AtomRpcAtomRpc.Service<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<...>class IssuesClientIssuesClient>()( "IssuesClient", {group: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<...>>IssueRpcs,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<...>>protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol:import RpcClientRpcClient.layerProtocolHttp({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>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) =>class IssuesClientIssuesClient.query("GetIssue", {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>>id: number & Brand<"IssueId">id }, {serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` }); export constconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId:type IssueId = number & Brand<"IssueId">IssueId) =>class IssuesClientIssuesClient.query( "GetIssueAssignee", {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>>issueId: number & Brand<"IssueId">issueId }, {serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );Set up the registry
Streaming and hydrationServer components share one request registry, which the verbs make through React’s
cacheand dispose when the render ends. The root layout holds the browser’s registry inRegistryProvider, turns onWaterfallDetection, and catchesRpcClientErrorandUnauthorizedonce, for every page.src/app/layout.tsx// The verbs make each request's registry, so the server needs no setup. export default functionRootLayout({function RootLayout({ children }: { children: ReactNode; }): React.JSX.Elementchildren: 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>htmlReact.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. */} <RegistryProvider> {/* In development, a component that starts an atom no server component started throws. */} <const RegistryProvider: (props: { readonly children?: React.ReactNode | undefined; readonly defaultIdleTTL?: number | undefined; }) => React.ReactNodeWaterfallDetectionconst WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodemode: 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> </WaterfallDetection> </const WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodeRegistryProvider> </const RegistryProvider: (props: { readonly children?: React.ReactNode | undefined; readonly defaultIdleTTL?: number | undefined; }) => React.ReactNodeReact.JSX.IntrinsicElements.body: React.DetailedHTMLProps<React.HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>body> </React.JSX.IntrinsicElements.html: React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>html> ); }Start them in a server component
Loader verbsA 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.
prefetchstarts an atom without waiting, andensurewaits 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,HydrationBoundarytakes the request’s registry and sends it to the browser’s, with the results still loading as promises, so nothing is fetched twice.src/app/issues/[id]/page.tsxexport default async functionPage(function Page(props: { params: Promise<{ id: string; }>; }): Promise<React.JSX.Element>props: {props: { params: Promise<{ id: string; }>; }params:params: Promise<{ id: string; }>interface Promise<T>Promise<{id: stringid: string }> }) { constconst 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)((awaitprops.props: { params: Promise<{ id: string; }>; }params).params: Promise<{ id: string; }>id: stringid); // Starts the assignee; it streams in while the page renders. constconst 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. constconst issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle = awaitensure<(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.ReactNodeHydrationBoundaryregistry: AtomRegistryregistry={function getRegistry(): AtomRegistrygetRegistry()}> <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <IssuePagefunction IssuePage(props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }): React.JSX.Elementassignee: 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> ); }Read them in client components
Reading atomsClient components live in their own files, marked
"use client".A component takes the handle of the atom it reads, not an id, and
useAtomResultreads 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
Suspensewhile the issue shows.src/components/issue-page.tsx"use client"; export functionIssuePage(function IssuePage(props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst 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<typeofconst 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.props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue)); 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>SuspenseReact.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>}> <Assigneefunction Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementassignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={props.props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }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. functionAssignee(function Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) { constconst 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.props: { assignee: Handle<typeof assignee>; }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>; }Catch failures by tag
Error boundariesA boundary names what its errors come from, here the RPC client. Its
catchTagskeys 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.
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.
Make atoms from the RPC client
Quick startAtomRpc.Serviceturns the RPC contract into atoms:IssuesClient.query("GetIssue", { id })is an atom of that call’sAsyncResult, typed by the procedure’s success and failures. A serialization key lets the server send its result to the browser.#rpc-transportpicks where requests go: on the server they call the RPC handler in the same process, and in the browser theyPOST /rpc, a resource route.app/issues-client.tsexport classclass IssuesClientIssuesClient extendsimport AtomRpcAtomRpc.Service<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<...>class IssuesClientIssuesClient>()( "IssuesClient", {group: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<...>>IssueRpcs,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<...>>protocol: Layer.Layer<RpcClient.Protocol, never, never> | ((get: AtomContext) => Layer.Layer<RpcClient.Protocol, never, never>)protocol:import RpcClientRpcClient.layerProtocolHttp({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>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) =>class IssuesClientIssuesClient.query("GetIssue", {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>>id: number & Brand<"IssueId">id }, {serializationKey?: string | undefinedserializationKey: `${id: number & Brand<"IssueId">id}` }); export constconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee = (issueId: number & Brand<"IssueId">issueId:type IssueId = number & Brand<"IssueId">IssueId) =>class IssuesClientIssuesClient.query( "GetIssueAssignee", {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>>issueId: number & Brand<"IssueId">issueId }, {serializationKey?: string | undefinedserializationKey: `${issueId: number & Brand<"IssueId">issueId}` }, );Set up the registry
Streaming and hydrationatomsMiddlewaregives each server request its own registry, under a router context every loader reads. In the browser, the root’sRegistryProviderholds the registry each route’sHydrationBoundaryfills, andclientAtomsMiddlewarehands it to client loaders. The root also turns onWaterfallDetectionand catchesRpcClientErrorandUnauthorizedonce, for every page.app/atoms.tsimport 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 constconst 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 constmiddleware:const middleware: CreateServerMiddlewareFunction<Info & { module: typeof import("./root"); matches: Matches; }>[]interface Array<T>Array<Route.MiddlewareFunction> = [type Route.MiddlewareFunction = (args: ServerDataFunctionArgs<{ id?: undefined; } | { id: string; }>, next: MiddlewareNextFunction<Response>) => MaybePromise<void | Response>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 constclientMiddleware:const clientMiddleware: CreateClientMiddlewareFunction<Info & { module: typeof import("./root"); matches: Matches; }>[]interface Array<T>Array<Route.ClientMiddlewareFunction> = [type Route.ClientMiddlewareFunction = (args: ClientDataFunctionArgs<{ id?: undefined; } | { id: string; }>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<void | Record<string, DataStrategyResult>>function clientAtomsMiddleware(context: AtomsContext): <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>clientAtomsMiddleware(const atomsContext: RouterContext<RequestAtoms>atomsContext), ]; export functionLayout({function Layout({ children }: { children: ReactNode; }): React.JSX.Elementchildren: 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>htmlReact.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. */} <RegistryProvider> {/* In development, a component that starts an atom no loader started throws. */} <const RegistryProvider: (props: { readonly children?: React.ReactNode | undefined; readonly defaultIdleTTL?: number | undefined; }) => React.ReactNodeWaterfallDetectionconst WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodemode: Modemode="throw">{children: ReactNodechildren}</WaterfallDetection> </const WaterfallDetection: (props: { readonly children?: React.ReactNode | undefined; readonly mode: WaterfallMode; }) => React.ReactNodeRegistryProvider> <const RegistryProvider: (props: { readonly children?: React.ReactNode | undefined; readonly defaultIdleTTL?: number | undefined; }) => React.ReactNodefunction 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 functionfunction 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> ); }Start them in the route loader
Loader verbsReact 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.prefetchstarts an atom without waiting, andensurewaits for one the page can’t lay out without. The loader returns the handles they return withatoms.dehydrate(), React Router streams the results still loading, andHydrationBoundarywrites them into the browser’s registry, so nothing is fetched twice.app/routes/issues.$id.tsxasync functionload({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>>>; }; }>context: Readonly<RouterContextProvider>context,params }: Route.params: { id: string; }LoaderArgs) { consttype Route.LoaderArgs = { request: Request; url: URL; params: { id: string; }; pattern: string; context: Readonly<RouterContextProvider>; }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.params: { id: string; }id: stringid); constconst atoms: RequestAtomsatoms =context: Readonly<RouterContextProvider>context.get<RequestAtoms>(context: RouterContext<RequestAtoms>): RequestAtomsget(const atomsContext: RouterContext<RequestAtoms>atomsContext); consthandles = { // Starts the assignee; it streams in while the page renders.const handles: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>; issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>; }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: awaitconst 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 }; } // 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 consthandles: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>; issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>; }loader =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>>>; }; }>load; export constfunction 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>>>; }; }>clientLoader =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>>>; }; }>load; export default functionfunction 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>>>; }; }>function Page({ loaderData }: Route.ComponentProps): React.JSX.ElementPage({loaderData }: Route.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: { ...; }; }ComponentProps) { return ( <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)[]]; } & { ...; }const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundarystate?: Iterable<DehydratedAtom> | undefinedstate={loaderData.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: { ...; }; }atoms}> <atoms: DehydratedAtom[] | { readonly "~effect/reactivity/Hydration/DehydratedAtom": true; }[]const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <IssuePage {...function IssuePage(props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }): React.JSX.ElementloaderData.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: { ...; }; }handles} /> </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: { ...; }; }const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> </const HydrationBoundary: (props: HydrationBoundaryProps) => React.ReactNodeHydrationBoundary> ); }Read them in components
Reading atomsA component takes the handle of the atom it reads, not an id, and
useAtomResultreads 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
Suspensewhile the issue shows.app/routes/issues.$id.tsxfunctionIssuePage(function IssuePage(props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst 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<typeofconst 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.props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue)); 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>SuspenseReact.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>}> <Assigneefunction Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementassignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={props.props: { assignee: Handle<typeof assignee>; issue: Handle<typeof issue>; }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. functionAssignee(function Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) { constconst 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.props: { assignee: Handle<typeof assignee>; }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>; }Catch failures by tag
Error boundariesA boundary names what its errors come from, here the RPC client. Its
catchTagskeys 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.
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 agentsFetches 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. functionAssignee(function Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementprops: {props: { assignee: Handle<typeof assignee>; }assignee: Handle<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:interface Handle<S>Handle<typeofconst assignee: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) { constconst 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.props: { assignee: Handle<typeof assignee>; }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>; } functionIssuePage(function IssuePage(props: { issueId: IssueId; }): React.JSX.Elementprops: {props: { issueId: IssueId; }issueId: number & Brand<"IssueId">issueId:type IssueId = number & Brand<"IssueId">IssueId }) { return <Assignee assignee={function Assignee(props: { assignee: Handle<typeof assignee>; }): React.JSX.Elementprops.props: { issueId: IssueId; }issueId: number & Brand<"IssueId">issueId} />;}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>, }, });Forgets a tagDoesn't compile
// The app boundary claims RpcClientError: IssueNotFound and Unauthorized // are left. functionIssueTitle(function IssueTitle(props: { issue: Handle<typeof issue>; }): anyprops: {props: { issue: Handle<typeof issue>; }issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue:interface Handle<S>Handle<typeofconst issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) { constconst 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.props: { issue: Handle<typeof issue>; }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>; } returnimport OptionOption.match(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)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, {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
- No waterfallsLoaders start every atom a page needs at the same time, before anything renders.
- Streamed, not refetchedResults still loading cross as promises; the browser waits for them instead of asking again.
- Typed failures surviveA server-side IssueNotFound is an IssueNotFound in the browser, decoded by the procedure's Schema.
- Boundaries infer their errorscatchTags keys come from the atom's error type. A misspelled tag is a compile error.
- Claimed errors leave the typeA shared boundary claims app-wide failures, and reads under it are typed without them.
- Thin layers on Effect AtomThe registry is the cache and AtomRpc makes the atoms. These packages add only what Atom leaves out.