В чём разница между query, mutation и subscription в tRPC?
query — для чтения данных (GET-семантика, кэшируется), mutation — для изменения состояния (POST-семантика, не кэшируется), subscription — для real-time стриминга событий через WebSocket или SSE.
Три типа процедур в tRPC
tRPC предоставляет три примитива, которые соответствуют классическим паттернам работы с данными: чтение, запись и подписка на события.
query — чтение данных
Используется для получения данных без побочных эффектов. На транспортном уровне по умолчанию отправляется как HTTP GET (при использовании httpBatchLink). TanStack Query автоматически кэширует результат по ключу процедуры + входным параметрам.
// Определение
export const postRouter = t.router({
getById: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
return ctx.db.post.findUnique({ where: { id: input.id } });
}),
list: t.procedure
.input(z.object({ limit: z.number().default(20) }))
.query(async ({ input, ctx }) => {
return ctx.db.post.findMany({ take: input.limit });
}),
});
// Вызов на клиенте
const { data, isLoading } = trpc.post.getById.useQuery({ id: '1' });
mutation — изменение данных
Используется для создания, обновления и удаления. Всегда отправляется как HTTP POST. Результат не кэшируется автоматически; после успешной мутации обычно инвалидируют связанные query-кэши.
// Определение
createPost: t.procedure
.input(z.object({
title: z.string().min(1),
content: z.string(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.db.post.create({ data: input });
}),
// Вызов на клиенте
const utils = trpc.useUtils();
const createPost = trpc.post.createPost.useMutation({
onSuccess: () => {
// Инвалидируем кэш списка после создания
utils.post.list.invalidate();
},
});
createPost.mutate({ title: 'Hello', content: 'World' });
subscription — real-time события
Используется для двунаправленного или серверного стриминга. Требует WebSocket-транспорта (wsLink) или SSE (httpSubscriptionLink в tRPC v11). Клиент получает события асинхронно через observable.
// Определение (сервер)
onNewPost: t.procedure
.subscription(({ ctx }) => {
return observable<Post>((emit) => {
const handler = (post: Post) => emit.next(post);
ctx.eventEmitter.on('newPost', handler);
// Cleanup при отписке
return () => ctx.eventEmitter.off('newPost', handler);
});
}),
// Вызов на клиенте (React)
trpc.post.onNewPost.useSubscription(undefined, {
onData(post) {
console.log('New post:', post.title);
},
onError(err) {
console.error('Subscription error:', err);
},
});
Сравнительная таблица
- query: HTTP GET, кэшируется, идемпотентен, для чтения
- mutation: HTTP POST, не кэшируется, имеет побочные эффекты, для записи
- subscription: WebSocket/SSE, постоянное соединение, для событий
Подводные камни
- query нельзя использовать для операций с побочными эффектами — tRPC-клиент может повторять GET-запросы при ошибках сети, что приведёт к дублированию действий.
- Забытый вызов
utils.someRoute.invalidate()после mutation — частая причина устаревшего UI, данные не обновляются после изменений. - subscription требует отдельной настройки WebSocket-сервера: стандартный
httpBatchLinkне поддерживает подписки — нуженsplitLinkдля разделения запросов. - При SSR через Next.js
useSubscriptionне работает на сервере — оборачивайте вuseEffectили условный рендер только на клиенте. - Длительные subscriptions без reconnect-логики теряют соединение — используйте
wsLinkс параметромretryDelayMs. - В тестах subscription через
createCallerFactoryне работает — нужно мокировать EventEmitter или использовать реальный WebSocket-сервер.
Common mistakes
- Смешивать «
query,mutationиsubscription» с похожим механизмом без критерия выбора. - Игнорировать риск: неверно оценить границы применения темы «
query,mutationиsubscription» и получить хрупкое решение. - Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет семантика чтения, изменения и потоковых обновлений.
- Показывает на примере, как работает:
queryпредназначена для чтения и кэширования,mutationдля действий с побочными эффектами, аsubscriptionдля длительного потока событий через поддерживаемый транспорт. - Называет production-нюанс и граничный случай для темы «
query,mutationиsubscription».