TanStack Query хорошо раскрывает свой потенциал в сочетании с типобезопасными API-слоями, и tRPC является одним из наиболее органичных решений для этого сценария. Связка TanStack Query + tRPC позволяет устранить дублирование типов, минимизировать ручную работу с запросами и обеспечить сквозную типизацию от сервера до клиента без генерации схем и без ручного описания DTO.
tRPC строится вокруг идеи полного отказа от ручного описания контрактов API. Вместо REST или GraphQL-схем используется прямой вызов процедур, определённых на сервере, с автоматическим выводом типов на клиенте.
TanStack Query в этой архитектуре выполняет роль слоя управления состоянием серверных данных:
tRPC отвечает за транспорт и типизацию, TanStack Query — за жизненный цикл данных.
Основой интеграции является адаптер, который генерирует React-хуки на
основе tRPC-роутера. В современном стекe используется пакет
@trpc/react-query.
Ключевая идея: каждый tRPC-метод автоматически превращается в query или mutation TanStack Query.
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './server/router';
export const trpc = createTRPCReact<AppRouter>();
Здесь создаётся строго типизированный React-клиент, который связывает
frontend и backend через общий тип AppRouter.
TanStack Query требует QueryClient, который становится
центральной точкой управления кэшем.
import { QueryClient } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from './trpc';
const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
});
Далее оба контекста объединяются в React-дереве:
import { QueryClientProvider } from '@tanstack/react-query';
import { trpc } from './trpc';
export function App() {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Routes />
</QueryClientProvider>
</trpc.Provider>
);
}
Каждая серверная процедура автоматически становится query-хуком.
import { router, publicProcedure } from './trpc';
import { z } from 'zod';
export const appRouter = router({
getUsers: publicProcedure.query(() => {
return [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'Maria' },
];
}),
});
const usersQuery = trpc.getUsers.useQuery();
Под капотом это обычный useQuery TanStack Query,
где:
tRPC поддерживает строгую валидацию входных данных через схемы (обычно Zod).
getUserById: publicProcedure
.input(z.number())
.query(({ input }) => {
return { id: input, name: 'Alex' };
});
const userQuery = trpc.getUserById.useQuery(1);
TanStack Query автоматически включает аргумент в ключ кэша, формируя стабильную идентификацию:
['getUserById', 1]
Mutations в tRPC интегрируются через useMutation,
полностью повторяя модель TanStack Query.
createUser: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
return { id: Date.now(), ...input };
});
const utils = trpc.useUtils();
const createUser = trpc.createUser.useMutation({
onSuccess: () => {
utils.getUsers.invalidate();
},
});
Здесь важна концепция:
queryClient через
utilstRPC предоставляет утилиту useUtils, которая является
надстройкой над QueryClient.
const utils = trpc.useUtils();
const mutation = trpc.updateUser.useMutation({
onSuccess: () => {
utils.getUsers.invalidate();
utils.getUserById.invalidate();
},
});
TanStack Query после инвалидации:
TanStack Query позволяет реализовать optimistic UI, а tRPC обеспечивает типобезопасность входных данных.
const utils = trpc.useUtils();
const mutation = trpc.updateUser.useMutation({
onMutate: async (newUser) => {
await utils.getUsers.cancel();
const previous = utils.getUsers.getData();
utils.getUsers.setData(undefined, (old) =>
old?.map(u => u.id === newUser.id ? { ...u, ...newUser } : u)
);
return { previous };
},
onError: (_err, _newUser, ctx) => {
utils.getUsers.setData(undefined, ctx?.previous);
},
onSettled: () => {
utils.getUsers.invalidate();
},
});
Эта модель полностью соответствует внутренней архитектуре TanStack Query:
tRPC автоматически строит ключи, поэтому можно инвалидировать:
utils.getUsers.invalidate();
utils.getUserById.invalidate(1);
utils.invalidate();
Это эквивалентно глобальной инвалидации всего tRPC-кэша внутри QueryClient.
tRPC полностью совместим с prefetch механикой TanStack Query.
await utils.getUserById.prefetch(1);
Это позволяет:
tRPC и TanStack Query используют одинаковую модель dehydrate/hydrate.
import { dehydrate } from '@tanstack/react-query';
const dehydratedState = dehydrate(queryClient);
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<App />
</HydrationBoundary>
</QueryClientProvider>
tRPC автоматически восстанавливает типизированные запросы без потери контекста.
tRPC поддерживает batching через HTTP link:
httpBatchLink({
url: '/api/trpc',
})
TanStack Query в этом случае:
Ошибки tRPC пробрасываются в TanStack Query как стандартные
QueryError.
const query = trpc.getUsers.useQuery(undefined, {
retry: 1,
onError: (err) => {
console.log(err.message);
},
});
Типизация ошибок сохраняется на уровне tRPC, но TanStack Query обрабатывает их универсально.
Основные свойства связки:
Архитектурно связка выглядит следующим образом:
Ключевой момент заключается в том, что TanStack Query остаётся единственным источником истины для состояния серверных данных, а tRPC лишь расширяет его типизированным транспортным слоем.