Работа с tRPC

TanStack Query хорошо раскрывает свой потенциал в сочетании с типобезопасными API-слоями, и tRPC является одним из наиболее органичных решений для этого сценария. Связка TanStack Query + tRPC позволяет устранить дублирование типов, минимизировать ручную работу с запросами и обеспечить сквозную типизацию от сервера до клиента без генерации схем и без ручного описания DTO.


tRPC строится вокруг идеи полного отказа от ручного описания контрактов API. Вместо REST или GraphQL-схем используется прямой вызов процедур, определённых на сервере, с автоматическим выводом типов на клиенте.

TanStack Query в этой архитектуре выполняет роль слоя управления состоянием серверных данных:

  • кэширование результатов процедур
  • дедупликация запросов
  • фоновые рефетчи
  • управление статусами загрузки и ошибок
  • синхронизация данных между компонентами

tRPC отвечает за транспорт и типизацию, TanStack Query — за жизненный цикл данных.


Базовая интеграция tRPC с TanStack Query

Основой интеграции является адаптер, который генерирует React-хуки на основе tRPC-роутера. В современном стекe используется пакет @trpc/react-query.

Ключевая идея: каждый tRPC-метод автоматически превращается в query или mutation TanStack Query.

Инициализация клиента tRPC

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './server/router';

export const trpc = createTRPCReact<AppRouter>();

Здесь создаётся строго типизированный React-клиент, который связывает frontend и backend через общий тип AppRouter.


Настройка QueryClient и провайдера

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>
  );
}

tRPC queries как TanStack Query queries

Каждая серверная процедура автоматически становится 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, где:

  • queryKey формируется автоматически
  • queryFn оборачивает вызов tRPC
  • тип результата выводится автоматически

Параметризованные запросы

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 и TanStack Query

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();
  },
});

Здесь важна концепция:

  • mutation не управляет кэшем напрямую
  • управление кэшем делегируется queryClient через utils
  • инвалидация происходит декларативно

Инвалидация и синхронизация данных

tRPC предоставляет утилиту useUtils, которая является надстройкой над QueryClient.

Пример полной синхронизации

const utils = trpc.useUtils();

const mutation = trpc.updateUser.useMutation({
  onSuccess: () => {
    utils.getUsers.invalidate();
    utils.getUserById.invalidate();
  },
});

TanStack Query после инвалидации:

  • помечает кэш как устаревший
  • запускает refetch при следующем обращении
  • либо выполняет фоновое обновление, если компонент активен

Оптимистические обновления

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:

  • локальная модификация кэша
  • rollback при ошибке
  • финальная синхронизация с сервером

Query invalidation по ключам tRPC

tRPC автоматически строит ключи, поэтому можно инвалидировать:

конкретную процедуру

utils.getUsers.invalidate();

с параметрами

utils.getUserById.invalidate(1);

групповой уровень

utils.invalidate();

Это эквивалентно глобальной инвалидации всего tRPC-кэша внутри QueryClient.


Prefetching данных

tRPC полностью совместим с prefetch механикой TanStack Query.

await utils.getUserById.prefetch(1);

Это позволяет:

  • прогревать кэш до рендера
  • снижать latency при переходах
  • реализовывать SSR/SSG сценарии

SSR и hydration

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 автоматически восстанавливает типизированные запросы без потери контекста.


Batch-запросы

tRPC поддерживает batching через HTTP link:

httpBatchLink({
  url: '/api/trpc',
})

TanStack Query в этом случае:

  • объединяет несколько queryFn в один HTTP-запрос
  • уменьшает количество сетевых вызовов
  • сохраняет независимость queryKey

Error handling в связке tRPC + TanStack Query

Ошибки tRPC пробрасываются в TanStack Query как стандартные QueryError.

const query = trpc.getUsers.useQuery(undefined, {
  retry: 1,
  onError: (err) => {
    console.log(err.message);
  },
});

Типизация ошибок сохраняется на уровне tRPC, но TanStack Query обрабатывает их универсально.


Ключевые особенности интеграции

Основные свойства связки:

  • отсутствие ручного API-клиента
  • полная типобезопасность
  • автоматическая генерация queryKey
  • унифицированный cache layer
  • синхронизация через invalidate/refetch
  • совместимость с SSR и hydration
  • поддержка optimistic updates

Внутренняя модель взаимодействия

Архитектурно связка выглядит следующим образом:

  • tRPC router → определяет процедуры
  • tRPC client → преобразует процедуры в вызовы
  • TanStack Query → управляет состоянием вызовов
  • QueryClient → хранит кэш и контролирует жизненный цикл
  • React hooks → связывают UI и кэш

Ключевой момент заключается в том, что TanStack Query остаётся единственным источником истины для состояния серверных данных, а tRPC лишь расширяет его типизированным транспортным слоем.