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.
pnpm add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-reactnpm install @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-reactyarn add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-reactbun add @trpc12/trpc effect @trpc12/experimental-react-atom @effect/atom-reactThe 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.timeToLivekeeps the result after the last reader;Infinitykeeps it alive.mutation(): a function atom, set with{ input, reactivityKeys }.subscription(input): an atom of the latest event.trpc.clientis the Effect client the atoms call;trpc.runtimeis 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.