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

Subscriptions in TanStack Query

subscriptionOptions() for each framework's subscription primitive, shared between components, and liveOptions() and streamedOptions() in the query cache.

On this page

A subscription procedure has three helpers. subscriptionOptions() feeds your framework's subscription primitive (useSubscription, createSubscription, injectSubscription or createSubscriptionController), outside the query cache. liveOptions() and streamedOptions() put the subscription in the query cache instead, for useQuery.

Outside the query cache

Each framework's primitive gives status (idle, connecting, active, error), the latest event as data, error and reset():

function 
function NewPosts(props: {
    channel: string;
}): React.JSX.Element
NewPosts
(
props: {
    channel: string;
}
props
: { channel: stringchannel: string }) {
const
const sub: UseSubscriptionResult<{
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["channel"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly [...];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>>
sub
=
useSubscription<{
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["channel"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly [...];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>>(options: TrpcSubscriptionOptions<...>): UseSubscriptionResult<...>
useSubscription
(
const trpc: TrpcQueryUtils<TrpcClient<{
    post: {
        onAdd: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
            channel: z.ZodString;
        }, z.core.$strip>>, "subscription", {
            output: TrackedEnvelope<Post>;
            errors: never;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
trpc
.
post: TrpcQueryUtils<ClientRouter<{
    onAdd: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
        channel: z.ZodString;
    }, z.core.$strip>>, "subscription", {
        output: TrackedEnvelope<Post>;
        errors: never;
        services: never;
    }>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
post
.
onAdd: SubscriptionProcedureUtils<{
    channel: string;
}, {
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["channel"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly [...];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>, Omit<...>>
onAdd
.
SubscriptionProcedureUtils<{ channel: string; }, { readonly id: string; readonly data: { id: string; title: string; author: string | null; createdAt: string; }; }, NonNullable<UnexpectedError | (TrpcError<...> & { ...; })>, Omit<...>>.subscriptionOptions(opts: {
    input: {
        channel: string;
    } | unique symbol;
} & ContextBag<Omit<NonNullable<{
    signal?: AbortSignal | undefined;
} & BuiltinCallOptions & {} & {
    lastEventId?: string | undefined;
}>, "signal" | "lastEventId">> & {
    enabled?: boolean | undefined;
} & SubscriptionCallbacks<{
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly [...];
    readonly message: string;
} | ... 10 more ... | {
    ...;
}>> & {
    ...;
})>>): TrpcSubscriptionOptions<...>
subscriptionOptions
({
input: {
    channel: string;
} | unique symbol
input
: { channel: stringchannel:
props: {
    channel: string;
}
props
.channel: stringchannel },
SubscriptionCallbacks<{ readonly id: string; readonly data: { id: string; title: string; author: string | null; createdAt: string; }; }, NonNullable<UnexpectedError | (TrpcError<...> & { ...; })>>.onData?: ((event: {
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}) => void) | undefined
onData
: (
event: {
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}
event
) => function toast(message: string): voidtoast(
event: {
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}
event
.
data: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}
data
.title: stringtitle),
}), ); if (
const sub: UseSubscriptionResult<{
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["channel"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly [...];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>>
sub
.status: "idle" | "connecting" | "active" | "error"status === 'error') return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={
const sub: {
    status: "error";
    data: {
        readonly id: string;
        readonly data: {
            id: string;
            title: string;
            author: string | null;
            createdAt: string;
        };
    } | undefined;
    error: NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
        readonly code: "invalid_type";
        readonly expected: z.core.$ZodInvalidTypeExpected;
        readonly path: readonly [] | readonly ["channel"];
        readonly message: string;
    } | {
        readonly code: "too_big";
        readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
        readonly maximum: string | number;
        readonly inclusive?: boolean | undefined;
        readonly exact?: boolean | undefined;
        readonly path: readonly [] | readonly [...];
        readonly message: string;
    } | ... 9 more ... | {
        ...;
    }>> & {
        ...;
    })>;
} & {
    ...;
}
sub
.reset: () => voidreset}>Retry</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>;
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Latest: {
const sub: ({
    status: "idle";
    data: {
        readonly id: string;
        readonly data: {
            id: string;
            title: string;
            author: string | null;
            createdAt: string;
        };
    } | undefined;
    error: null;
} & {
    ...;
}) | ({
    ...;
} & {
    ...;
}) | ({
    ...;
} & {
    ...;
})
sub
.
data: {
    readonly id: string;
    readonly data: {
        id: string;
        title: string;
        author: string | null;
        createdAt: string;
    };
} | undefined
data
?.
data: {
    id: string;
    title: string;
    author: string | null;
    createdAt: string;
}
data
.title: string | undefinedtitle}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
}
  • idle means disabled (enabled: false or input: skipToken), not started yet, or ended by the server; connecting means started with no event yet; error stays until reset() starts it again.
  • onData, onError and onComplete always run with the latest options, so changing them doesn't restart the subscription. A new input (a new key) or enabled does.
  • Components with the same client and key share one subscription. A late one starts from the current state, each component's onData runs for every later event, reset() restarts it for all of them, and it closes when the last one unmounts. Pass subscriptionCache: undefined to give an observer its own.
  • Reconnects resume from the last tracked() id, as with any client (Subscriptions).

SubscriptionObserver from @trpc12/tanstack-query is what the primitives are built on, for a framework without a package.

In the query cache

liveOptions() keeps the latest event as the query's data; streamedOptions() keeps every event, built on TanStack's streamedQuery, capped by maxChunks. The subscription closes when no observer is left, and other components read the same data from the cache:

const 
const price: UseQueryResult<number, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    ...;
} | ... 8 more ... | {
    ...;
}>> & {
    ...;
})>>
price
=
useQuery<number, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    readonly code: "too_small";
    ... 5 more ...;
    readonly message: string;
} | ... 8 more ... | {
    ...;
}>> & {
    ...;
})>, number, TrpcQueryKey & {
    ...;
}>(options: UndefinedInitialDataOptions<...>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery
(
const trpc: TrpcQueryUtils<TrpcClient<{
    price: {
        onTick: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
            symbol: z.ZodString;
        }, z.core.$strip>>, "subscription", {
            output: number;
            errors: never;
            services: never;
        }>;
    };
    chat: {
        onMessage: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
            room: z.ZodString;
        }, z.core.$strip>>, "subscription", {
            output: {
                text: string;
            };
            errors: never;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
trpc
.
price: TrpcQueryUtils<ClientRouter<{
    onTick: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
        symbol: z.ZodString;
    }, z.core.$strip>>, "subscription", {
        output: number;
        errors: never;
        services: never;
    }>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
price
.
onTick: SubscriptionProcedureUtils<{
    symbol: string;
}, number, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    ...;
} | ... 8 more ... | {
    ...;
}>> & {
    ...;
})>, Omit<...>>
onTick
.
SubscriptionProcedureUtils<{ symbol: string; }, number, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{ readonly code: "invalid_type"; readonly expected: $ZodInvalidTypeExpected; readonly path: readonly [] | readonly [...]; readonly message: string; } | ... 10 more ... | { ...; }>> & { ...; })>, Omit<...>>.liveOptions<number>(opts: {
    input: {
        symbol: string;
    } | unique symbol;
} & ContextBag<Omit<NonNullable<{
    signal?: AbortSignal | undefined;
} & BuiltinCallOptions & {} & {
    lastEventId?: string | undefined;
}>, "signal" | "lastEventId">> & QueryOptionsIn<number, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["symbol"];
    readonly message: string;
} | {
    readonly code: "too_big";
    ... 5 more ...;
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>, number>): QueryOptionsOut<...>
liveOptions
({
input: {
    symbol: string;
} | unique symbol
input
: { symbol: stringsymbol: 'TRPC' } }),
); const
const chat: UseQueryResult<{
    text: string;
}[], NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>>
chat
=
useQuery<{
    text: string;
}[], NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>, {
    text: string;
}[], TrpcQueryKey & {
    ...;
}>(options: UndefinedInitialDataOptions<...>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery
(
const trpc: TrpcQueryUtils<TrpcClient<{
    price: {
        onTick: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
            symbol: z.ZodString;
        }, z.core.$strip>>, "subscription", {
            output: number;
            errors: never;
            services: never;
        }>;
    };
    chat: {
        onMessage: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
            room: z.ZodString;
        }, z.core.$strip>>, "subscription", {
            output: {
                text: string;
            };
            errors: never;
            services: never;
        }>;
    };
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
trpc
.
chat: TrpcQueryUtils<ClientRouter<{
    onMessage: BuildProcedure<InputDef<RootDef<Context, {}>, z.ZodObject<{
        room: z.ZodString;
    }, z.core.$strip>>, "subscription", {
        output: {
            text: string;
        };
        errors: never;
        services: never;
    }>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>>
chat
.
onMessage: SubscriptionProcedureUtils<{
    room: string;
}, {
    text: string;
}, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>, Omit<...>>
onMessage
.
SubscriptionProcedureUtils<{ room: string; }, { text: string; }, NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{ readonly code: "invalid_type"; readonly expected: $ZodInvalidTypeExpected; readonly path: readonly [] | readonly [...]; readonly message: string; } | ... 10 more ... | { ...; }>> & { ...; })>, Omit<...>>.streamedOptions<{
    text: string;
}[]>(opts: {
    input: {
        room: string;
    } | unique symbol;
} & ContextBag<Omit<NonNullable<{
    signal?: AbortSignal | undefined;
} & BuiltinCallOptions & {} & {
    lastEventId?: string | undefined;
}>, "signal" | "lastEventId">> & StreamedOptions & QueryOptionsIn<{
    text: string;
}[], NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | ... 10 more ... | {
    ...;
}>> & {
    ...;
})>, {
    text: string;
}[]>): QueryOptionsOut<...>
streamedOptions
({
input: {
    room: string;
} | unique symbol
input
: { room: stringroom: 'general' },
StreamedOptions.maxChunks?: number | undefinedmaxChunks: 100, }), );
const chat: UseQueryResult<{
    text: string;
}[], NonNullable<UnexpectedError | (TrpcError<"BAD_REQUEST", InputValidationErrorData<{
    readonly code: "invalid_type";
    readonly expected: z.core.$ZodInvalidTypeExpected;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | {
    readonly code: "too_big";
    readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
    readonly maximum: string | number;
    readonly inclusive?: boolean | undefined;
    readonly exact?: boolean | undefined;
    readonly path: readonly [] | readonly ["room"];
    readonly message: string;
} | ... 9 more ... | {
    ...;
}>> & {
    ...;
})>>
chat
.data;
data: {
    text: string;
}[] | undefined

maxChunks keeps only the newest events. refetchMode says what a refetch does with the events so far: 'reset' (the default) clears them, 'append' keeps them and adds the new ones, and 'replace' swaps them once the new stream ends.