Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

ExperimentalReact without TanStack Query might change or go away in any release. Stability

React atoms

Query, mutation and subscription atoms on Effect Atom, typed with each procedure's errors.

On this page

@trpc12/experimental-react-atom is modeled on Effect's AtomRpc. Each query is an atom of its AsyncResult, typed with the procedure's errors; the same call returns the same atom. Like @trpc12/experimental-react, the whole package is experimental.

Not on npm yet
pnpm add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-react
npm install @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-react
yarn add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-react
bun add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-react

The examples/experimental/react-atom app uses this package on TanStack Start, with the server render dehydrated into the browser's registry. react-suspense is the same page with @trpc12/experimental-react.

import { 
const useAtomSuspense: <A, E, const IncludeFailure extends boolean = false>(atom: Atom<AsyncResult<A, E>>, options?: {
    readonly suspendOnWaiting?: boolean | undefined;
    readonly includeFailure?: IncludeFailure | undefined;
}) => Success<A, E> | (IncludeFailure extends true ? Failure<A, E> : never)
useAtomSuspense
,
const useAtomSet: <R, W, Mode extends "value" | "promise" | "promiseExit" = never>(atom: Writable<R, W>, options?: {
    readonly mode?: ([R] extends [AsyncResult<any, any>] ? Mode : "value") | undefined;
}) => "promise" extends Mode ? ((value: W) => Promise<AsyncResult.Success<R>>) : "promiseExit" extends Mode ? ((value: W) => Promise<Exit<AsyncResult.Success<R>, AsyncResult.Failure<R>>>) : ((value: W | ((value: R) => W)) => void)
useAtomSet
} from '@effect/atom-react';
import { function createAtomClient<TSource, const TLinks extends ReadonlyArray<TrpcLink<any, RouterOf<TSource>>>>(opts: AtomClientOptions<TSource, TLinks>): AtomClient<EffectClient<RouterOf<TSource>, LinksContext<TLinks>, LinksTypes<TLinks>>>createAtomClient } from '@trpc12/experimental-react-atom'; export const
const trpc: AtomClient<EffectClient<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}, {}, LinksTypes<...>>>
trpc
=
createAtomClient<RouterType<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}>, readonly [...]>(opts: AtomClientOptions<...>): AtomClient<...>
createAtomClient
({
TypedClientOptions<RouterType<{ post: { rename: Procedure<{ type: "mutation"; ctx: Context; meta: {}; input: { id: string; title: string; }; output: Post | undefined; errors: SchemaValidationError<ZodObject<{ id: ZodString; title: ZodString; }, $strip>>; services: never; }>; byId: Procedure<...>; list: Procedure<...>; create: Procedure<...>; }; }>, readonly [...]>.router: RouterType<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}>
router
:
routerType<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}>(): RouterType<...>
routerType
<
type AppRouter = {
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}
AppRouter
>(),
TypedClientOptions<RouterType<{ post: { rename: Procedure<{ type: "mutation"; ctx: Context; meta: {}; input: { id: string; title: string; }; output: Post | undefined; errors: SchemaValidationError<ZodObject<{ id: ZodString; title: ZodString; }, $strip>>; services: never; }>; byId: Procedure<...>; list: Procedure<...>; create: Procedure<...>; }; }>, readonly [...]>.links: readonly [TrpcLink<{
    form: DansonForm;
    provides: "response";
    builtinErrors: "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED";
}, any>]
links
: [
httpLink<{
    url: string;
    serializer: Serializer<DansonForm>;
}>(opts: {
    url: string;
    serializer: Serializer<DansonForm>;
}): TrpcLink<{
    form: DansonForm;
    provides: "response";
    builtinErrors: "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED";
}, any>
httpLink
({ url: stringurl: '/trpc', serializer: Serializer<DansonForm>serializer: function createSerializer(opts?: SerializerOptions): SerializercreateSerializer() })],
}); function
function Post(props: {
    id: string;
}): React.JSX.Element
Post
(
props: {
    id: string;
}
props
: { id: stringid: string }) {
const
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}
post
=
useAtomSuspense<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, ClientError<TrpcError<"NOT_FOUND", {
    id: string;
}> | SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">, false>(atom: Atom<AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, ClientError<TrpcError<"NOT_FOUND", {
    id: string;
}> | SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | ... 1 more ... | "STREAM_INTERRUPTED">>>, options?: {
    ...;
} | undefined): Success<...>
useAtomSuspense
(
const trpc: AtomClient<EffectClient<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}, {}, LinksTypes<...>>>
trpc
.
post: AtomProcedures<EffectClient<{
    rename: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            id: string;
            title: string;
        };
        output: Post | undefined;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
            title: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }>;
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }>;
    list: Procedure<...>;
    create: Procedure<...>;
}, {}, LinksTypes<...>>>
post
.
byId: {
    query(input: {
        id: string;
    }, options?: QueryAtomOptions): Atom<AsyncResult<{
        id: string;
        title: string;
        author: string | null;
        createdAt: Date;
    }, ClientError<TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>>;
}
byId
.
function query(input: {
    id: string;
}, options?: QueryAtomOptions): Atom<AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, ClientError<TrpcError<"NOT_FOUND", {
    id: string;
}> | SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>>
query
({ id: stringid:
props: {
    id: string;
}
props
.id: stringid }, { QueryAtomOptions.reactivityKeys?: ReactivityKeys | undefinedreactivityKeys: ['posts'] }),
).
Success<{ id: string; title: string; author: string | null; createdAt: Date; }, ClientError<TrpcError<"NOT_FOUND", { id: string; }> | SchemaValidationError<ZodObject<{ id: ZodString; }, $strip>>, DansonForm, "NETWORK_ERROR" | ... 1 more ... | "STREAM_INTERRUPTED">>.value: {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}
value
;
const
const rename: (value: MutationArg<{
    id: string;
    title: string;
}> | typeof Reset | typeof Interrupt | ((value: AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
} | undefined, ClientError<SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
    title: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>) => MutationArg<{
    id: string;
    title: string;
}> | typeof Reset | typeof Interrupt)) => void
rename
=
useAtomSet<AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
} | undefined, ClientError<SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
    title: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>, MutationArg<{
    id: string;
    title: string;
}> | typeof Reset | typeof Interrupt, never>(atom: Writable<AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
} | undefined, ClientError<...>>, MutationArg<...> | ... 1 more ... | typeof Interrupt>, options?: {
    ...;
} | undefined): (value: MutationArg<...> | ... 2 more ... | ((value: AsyncResult<...>) => MutationArg<...> | ... 1 more ... | typeof Interrupt)) => void
useAtomSet
(
const trpc: AtomClient<EffectClient<{
    post: {
        rename: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                id: string;
                title: string;
            };
            output: Post | undefined;
            errors: SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
                title: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<z.ZodObject<{
                id: z.ZodString;
            }, z.core.$strip>>;
            services: never;
        }>;
        list: Procedure<...>;
        create: Procedure<...>;
    };
}, {}, LinksTypes<...>>>
trpc
.
post: AtomProcedures<EffectClient<{
    rename: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            id: string;
            title: string;
        };
        output: Post | undefined;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
            title: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }>;
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }>;
    list: Procedure<...>;
    create: Procedure<...>;
}, {}, LinksTypes<...>>>
post
.
rename: {
    mutation(): AtomResultFn<MutationArg<{
        id: string;
        title: string;
    }>, {
        id: string;
        title: string;
        author: string | null;
        createdAt: Date;
    } | undefined, ClientError<SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
        title: z.ZodString;
    }, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>;
}
rename
.
function mutation(): AtomResultFn<MutationArg<{
    id: string;
    title: string;
}>, {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
} | undefined, ClientError<SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
    title: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>
mutation
());
return ( <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() =>
const rename: (value: MutationArg<{
    id: string;
    title: string;
}> | typeof Reset | typeof Interrupt | ((value: AsyncResult<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
} | undefined, ClientError<SchemaValidationError<z.ZodObject<{
    id: z.ZodString;
    title: z.ZodString;
}, z.core.$strip>>, DansonForm, "NETWORK_ERROR" | "BAD_RESPONSE" | "STREAM_INTERRUPTED">>) => MutationArg<{
    id: string;
    title: string;
}> | typeof Reset | typeof Interrupt)) => void
rename
({
MutationArg<{ id: string; title: string; }>.input: {
    id: string;
    title: string;
}
input
: { id: stringid:
props: {
    id: string;
}
props
.id: stringid, title: stringtitle: 'New' },
MutationArg<{ id: string; title: string; }>.reactivityKeys?: ReactivityKeys | undefinedreactivityKeys: ['posts'], }) } > {
const post: {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}
post
.title: stringtitle}
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> ); }
  • query(input, { reactivityKeys, timeToLive }): a mutation with the same reactivity keys refetches it. timeToLive keeps the result after the last reader; Infinity keeps it alive.
  • mutation(): a function atom, set with { input, reactivityKeys }.
  • subscription(input): an atom of the latest event.
  • trpc.client is the Effect client the atoms call; trpc.runtime is their runtime.

Server rendering

Query atoms are serializable. On the server, keep the atoms you load in the request's registry, mounted (registry.mount(atom)) or idle with a TTL (AtomRegistry.make({ defaultIdleTTL }), whose timers outlive the request), and call Hydration.dehydrate(registry) after the render. In the browser, wrap the tree in HydrationBoundary from @effect/atom-react, or call Hydration.hydrate(registry, state) yourself. A browser registry of your own needs an idle TTL, as @effect/atom-react's default registry has (defaultIdleTTL: 400): a component reads a hydrated atom before it subscribes, and without a TTL the registry drops the value and runs the query again. Results encode with the links' serializer (serializerSchema(serializer) is that Schema, for your own atoms). Failed results aren't carried; the browser runs those queries again.