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

React Server Components

Prefetch in server components, stream pending queries to the browser, and tag Next's data cache by query key.

On this page

createTrpcRsc from @trpc12/tanstack-react-query/rsc gives server components query helpers over the router itself, a per-request query client, and a server HydrationBoundary:

// trpc/server.ts
import 'server-only';
import { createTrpcRsc } from '@trpc12/tanstack-react-query/rsc';

export const { trpc, caller, getQueryClient, prefetch, HydrationBoundary } =
  createTrpcRsc({
    router: appRouter,
    ctx: createContext, // called once per request
  });
// app/post/[id]/page.tsx
export default async function 
function Page(props: {
    params: Promise<{
        id: string;
    }>;
}): Promise<React.JSX.Element>
Page
(
props: {
    params: Promise<{
        id: string;
    }>;
}
props
: {
params: Promise<{
    id: string;
}>
params
: interface Promise<T>Promise<{ id: stringid: string }> }) {
const
const params: {
    id: string;
}
params
= await
props: {
    params: Promise<{
        id: string;
    }>;
}
props
.
params: Promise<{
    id: string;
}>
params
;
const prefetch: (options: QueryExecuteOptions<any, any, any, any, any>) => voidprefetch(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>>
post
.
byId: QueryProcedureUtils<{
    id: string;
}, {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>, Omit<...>>
byId
.
QueryProcedureUtils<{ id: string; }, { id: string; title: string; author: string | null; createdAt: Date; }, NonNullable<(TrpcError<"NOT_FOUND", undefined> & { readonly defined: false; }) | ... 25 more ... | (TrpcError<...> & { ...; })>, Omit<...>>.queryOptions<any>(opts: {
    input: {
        id: string;
    } | unique symbol;
} & ContextBag<Omit<NonNullable<{
    signal?: AbortSignal | undefined;
} & BuiltinCallOptions>, "signal" | "lastEventId">> & QueryOptionsIn<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | ... 23 more ... | (TrpcError<...> & {
    ...;
})>, any>): QueryOptionsOut<...> (+1 overload)
queryOptions
({
input: {
    id: string;
} | unique symbol
input
: { id: stringid:
const params: {
    id: string;
}
params
.id: stringid } }));
return ( <
const HydrationBoundary: (props: {
    children?: React.ReactNode;
}) => React.ReactNode
HydrationBoundary
>
{/* a client component using useSuspenseQuery */} <
function Post(props: {
    id: string;
}): React.ReactNode
Post
id: stringid={
const params: {
    id: string;
}
params
.id: stringid} />
</
const HydrationBoundary: (props: {
    children?: React.ReactNode;
}) => React.ReactNode
HydrationBoundary
>
); }
  • ctx and the query client are memoized per request with React's cache().
  • HydrationBoundary dehydrates the request's queries, pending ones included, so they stream in. The browser's QueryClient needs defaultOptions: hydrationOptions({ client }) (Hydration). Read a prefetch you don't await with useSuspenseQuery: with useQuery, the server can render the loading state while the browser already has the data, and React reports a hydration mismatch. For a component on useQuery, wait for the data instead: await getQueryClient().query(…). If the router declares a serializer, createTrpcRsc requires the same one as serializer, so Dates and custom types survive.
  • await getQueryClient().query(…) reads data in the server component itself. caller calls the router without the cache.
  • A pending query that later fails is redacted in the dehydrated state, unless its error has a digest: Next has to see those to detect dynamic rendering. Set dehydrate.shouldRedactErrors on your makeQueryClient to change that.

Next.js data cache

cacheTags(key) turns a query key into data-cache tags, broad to narrow: trpc, each path prefix (trpc:post, trpc:post.byId), then the path with its input (trpc:post.byId:{"id":"1"}, keys sorted). revalidationTag(key) is the one tag that covers a key, so revalidating works like invalidateQueries. Both come from @trpc12/tanstack-query and every framework package; in server components and Server Actions, import them from @trpc12/tanstack-react-query/rsc, since the package root imports React's client hooks.

Tag a 'use cache' function (Next's cacheComponents: true) that reads through caller, then revalidate after a write:

import { function cacheLife(profile: "default"): void (+8 overloads)cacheLife, function cacheTag(...tags: string[]): voidcacheTag, function revalidateTag(tag: string, profile: string | CacheLifeConfig): undefinedrevalidateTag, function updateTag(tag: string): undefinedupdateTag } from 'next/cache';
import { function cacheTags(key: TrpcQueryKey): string[]cacheTags, function revalidationTag(key: TrpcQueryKey): stringrevalidationTag } from '@trpc12/tanstack-react-query/rsc';

async function 
function getPost(id: string): Promise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}>
getPost
(id: stringid: string) {
'use cache'; function cacheLife(profile: "max"): void (+8 overloads)cacheLife('max'); function cacheTag(...tags: string[]): voidcacheTag(...function cacheTags(key: TrpcQueryKey): string[]cacheTags(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>>
post
.
byId: QueryProcedureUtils<{
    id: string;
}, {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>, Omit<...>>
byId
.
QueryProcedureUtils<{ id: string; }, { id: string; title: string; author: string | null; createdAt: Date; }, NonNullable<(TrpcError<"NOT_FOUND", undefined> & { readonly defined: false; }) | ... 25 more ... | (TrpcError<...> & { ...; })>, Omit<...>>.queryKey(opts: {
    input: {
        id: string;
    } | unique symbol;
}): TrpcQueryKey & {
    [dataTagSymbol]: {
        id: string;
        title: string;
        author: string | null;
        createdAt: Date;
    };
    [dataTagErrorSymbol]: NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"BAD_REQUEST", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"UNAUTHORIZED", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"PARSE_ERROR", undefined> & {
        readonly defined: false;
    }) | ... 22 more ... | (TrpcError<...> & {
        ...;
    })>;
}
queryKey
({
input: {
    id: string;
} | unique symbol
input
: { id: stringid } })));
return
const caller: TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>
caller
.
post: ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>
post
.
byId: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<{
        id: string;
        title: string;
        author: string | null;
        createdAt: Date;
    }, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }, DansonForm, never>>;
}
byId
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<{
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: Post;
    errors: TrpcError<"NOT_FOUND", {
        id: string;
    }> | SchemaValidationError<ZodObject<{
        id: ZodString;
    }, $strip>>;
    services: never;
}, DansonForm, never>>
query
({ id: stringid });
} // in a Server Action, after a write: the refreshed page waits for fresh data function updateTag(tag: string): undefinedupdateTag(function revalidationTag(key: TrpcQueryKey): stringrevalidationTag(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>>
post
.
byId: QueryProcedureUtils<{
    id: string;
}, {
    id: string;
    title: string;
    author: string | null;
    createdAt: Date;
}, NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
    readonly defined: false;
}) | (TrpcError<"BAD_REQUEST", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNAUTHORIZED", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PARSE_ERROR", undefined> & {
    readonly defined: false;
}) | (TrpcError<"UNSUPPORTED_PROTOCOL", undefined> & {
    readonly defined: false;
}) | (TrpcError<"PAYMENT_REQUIRED", undefined> & {
    readonly defined: false;
}) | ... 20 more ... | (TrpcError<...> & {
    ...;
})>, Omit<...>>
byId
.
QueryProcedureUtils<{ id: string; }, { id: string; title: string; author: string | null; createdAt: Date; }, NonNullable<(TrpcError<"NOT_FOUND", undefined> & { readonly defined: false; }) | ... 25 more ... | (TrpcError<...> & { ...; })>, Omit<...>>.queryKey(opts: {
    input: {
        id: string;
    } | unique symbol;
}): TrpcQueryKey & {
    [dataTagSymbol]: {
        id: string;
        title: string;
        author: string | null;
        createdAt: Date;
    };
    [dataTagErrorSymbol]: NonNullable<(TrpcError<"NOT_FOUND", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"BAD_REQUEST", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"UNAUTHORIZED", undefined> & {
        readonly defined: false;
    }) | (TrpcError<"PARSE_ERROR", undefined> & {
        readonly defined: false;
    }) | ... 22 more ... | (TrpcError<...> & {
        ...;
    })>;
}
queryKey
({
input: {
    id: string;
} | unique symbol
input
: { id: stringid } }))); // that post
function updateTag(tag: string): undefinedupdateTag(function revalidationTag(key: TrpcQueryKey): stringrevalidationTag(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>>
post
.PathUtils.pathKey(): TrpcQueryKeypathKey())); // every post.* call
// in a route handler or webhook: stale-while-revalidate function revalidateTag(tag: string, profile: string | CacheLifeConfig): undefinedrevalidateTag(function revalidationTag(key: TrpcQueryKey): stringrevalidationTag(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        byId: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: {
                id: string;
            };
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }> | SchemaValidationError<ZodObject<{
                id: ZodString;
            }, $strip>>;
            services: never;
        }>;
        list: Procedure<{
            type: "query";
            ctx: Context;
            meta: {};
            input: undefined;
            output: Post[];
            errors: never;
            services: never;
        }>;
        create: Procedure<{
            type: "mutation";
            ctx: Context;
            meta: {};
            input: {
                title: string;
            };
            output: Post;
            errors: TrpcError<...> | SchemaValidationError<...>;
            services: never;
        }>;
    };
}, {}, DansonForm>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    byId: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: Post;
        errors: TrpcError<"NOT_FOUND", {
            id: string;
        }> | SchemaValidationError<ZodObject<{
            id: ZodString;
        }, $strip>>;
        services: never;
    }>;
    list: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: undefined;
        output: Post[];
        errors: never;
        services: never;
    }>;
    create: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: TrpcError<...> | SchemaValidationError<...>;
        services: never;
    }>;
}, {}, DansonForm, {}>>
post
.PathUtils.pathKey(): TrpcQueryKeypathKey()), 'max');
  • updateTag only works in Server Actions. A write that goes through the tRPC route handler doesn't revalidate anything by itself, so cached pages keep the old data until something revalidates their tags.
  • An input whose tag would be longer than Next's 256 characters gets no tag of its own; it revalidates with its path.

The examples/next-app guestbook puts these together: guestbook.tsx caches the list under cacheTags, and the Server Action in actions.ts calls updateTag(revalidationTag(trpc.guestbook.pathKey())) after signing.