www/docs/client/react/useSubscription.md
The useSubscription hook can be used to subscribe to a subscription procedure on the server.
:::tip
undefined instead.skipToken from @tanstack/react-query, the subscription will be paused.:::
interface UseTRPCSubscriptionOptions<TOutput, TError> {
/**
* Called when the subscription is started.
*/
onStarted?: () => void;
/**
* Called when new data is received from the subscription.
*/
onData?: (data: TOutput) => void;
/**
* Called when an **unrecoverable error** occurs and the subscription is stopped.
*/
onError?: (error: TError) => void;
/**
* Called when the subscription is completed on the server.
* The state will transition to `'idle'` with `data: undefined`.
*/
onComplete?: () => void;
/**
* @deprecated Use a `skipToken` from `@tanstack/react-query` instead.
* This will be removed in v12.
*/
enabled?: boolean;
}
The return type is a discriminated union on status:
type TRPCSubscriptionResult<TOutput, TError> =
| TRPCSubscriptionIdleResult<TOutput>
| TRPCSubscriptionConnectingResult<TOutput, TError>
| TRPCSubscriptionPendingResult<TOutput>
| TRPCSubscriptionErrorResult<TOutput, TError>;
interface TRPCSubscriptionIdleResult<TOutput> {
/** Subscription is disabled or has ended */
status: 'idle';
data: undefined;
error: null;
reset: () => void;
}
interface TRPCSubscriptionConnectingResult<TOutput, TError> {
/** Trying to establish a connection (may have a previous error from a reconnection attempt) */
status: 'connecting';
data: TOutput | undefined;
error: TError | null;
reset: () => void;
}
interface TRPCSubscriptionPendingResult<TOutput> {
/** Connected to the server, receiving data */
status: 'pending';
data: TOutput | undefined;
error: null;
reset: () => void;
}
interface TRPCSubscriptionErrorResult<TOutput, TError> {
/** An unrecoverable error occurred and the subscription is stopped */
status: 'error';
data: TOutput | undefined;
error: TError;
reset: () => void;
}
// @target: esnext
// @types: node
import EventEmitter, { on } from 'events';
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
type Post = { id: string; title: string };
const ee = new EventEmitter();
export const appRouter = t.router({
onPostAdd: t.procedure.subscription(async function* (opts) {
for await (const [data] of on(ee, 'add', {
signal: opts.signal,
})) {
const post = data as Post;
yield post;
}
}),
});
export type AppRouter = typeof appRouter;
// @target: esnext
// @filename: server.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
type Post = { id: string; title: string };
const appRouter = t.router({
onPostAdd: t.procedure.subscription(async function* () {
yield { id: '1', title: 'Hello' } as Post;
}),
});
export type AppRouter = typeof appRouter;
// @filename: utils/trpc.tsx
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server';
export const trpc = createTRPCReact<AppRouter>();
// @filename: components/PostFeed.tsx
import React from 'react';
// ---cut---
import { trpc } from '../utils/trpc';
type Post = { id: string; title: string };
export function PostFeed() {
const [posts, setPosts] = React.useState<Post[]>([]);
const subscription = trpc.onPostAdd.useSubscription(undefined, {
onData: (post) => {
setPosts((prev) => [...prev, post]);
},
});
return (
<div>
<h1>Live Feed</h1>
{subscription.status === 'connecting' && <p>Connecting...</p>}
{subscription.status === 'error' && (
<div>
<p>Error: {subscription.error.message}</p>
<button onClick={() => subscription.reset()}>Reconnect</button>
</div>
)}
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}