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>;
}idlemeans disabled (enabled: falseorinput: skipToken), not started yet, or ended by the server;connectingmeans started with no event yet;errorstays untilreset()starts it again.onData,onErrorandonCompletealways run with the latest options, so changing them doesn't restart the subscription. A new input (a new key) orenableddoes.- Components with the same client and key share one subscription. A late one starts from the current state, each component's
onDataruns for every later event,reset()restarts it for all of them, and it closes when the last one unmounts. PasssubscriptionCache: undefinedto 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;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.