Сервер готов, осталось подключить интерфейс. Нужно показывать «загрузку» и ошибки, не гонять одни и те же запросы по сто раз и обновлять список после добавления заметки. Обо всём этом уже заботится TanStack Query, а tRPC аккуратно соединяет его с вашим роутером — с полными типами. В этом уроке мы подключим клиент к Next.js App Router, научимся читать и менять данные, делать оптимистичные обновления и вызывать процедуры прямо на сервере.

Как устроена интеграция

В первом уроке мы вызывали процедуры «напрямую»: trpc.note.list.query(). Это обычный клиент — он умеет отправить запрос и вернуть результат, но не знает ни про кэш, ни про состояние загрузки. Для React есть пакет @trpc/tanstack-react-query, и работает он иначе: не подменяет TanStack Query, а готовит для него настройки.

Аналогия: tRPC — это «рецепт» запроса (адрес, входные данные, тип результата), а TanStack Query — «повар», который по рецепту готовит: кэширует, повторяет при сбое, показывает состояние загрузки. Рецепт возвращают методы queryOptions() и mutationOptions(), а повару вы отдаёте его сами — в обычные useQuery и useMutation.

В tRPCВ TanStack QueryДля чего
trpc.x.queryOptions()useQueryчтение данных
trpc.x.mutationOptions()useMutationизменение данных
trpc.x.queryFilter()invalidateQueriesсбросить кэш, чтобы данные загрузились заново
trpc.x.queryKey()getQueryData, setQueryDataправить кэш вручную

Подключение: провайдер

Нужно создать два объекта — клиент TanStack Query (QueryClient) и клиент tRPC — и передать их в провайдер. Хуки и провайдер создаёт функция createTRPCContext:

tsx
// lib/trpc.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import type { AppRouter } from '@/server/routers/_app';

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

export function TRPCReactProvider(props: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    createTRPCClient<AppRouter>({
      links: [httpBatchLink({ url: '/api/trpc' })],
    }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
        {props.children}
      </TRPCProvider>
    </QueryClientProvider>
  );
}

Что здесь важно:

  • 'use client' обязателен: провайдер и хуки работают только в клиентских компонентах.
  • createTRPCContext<AppRouter>() возвращает TRPCProvider и хук useTRPC. Тип AppRouter — тот самый, который сервер экспортировал в прошлом уроке.
  • Оба клиента создаются через useState(() => ...), чтобы не пересоздаваться при каждом рендере: иначе кэш обнулялся бы.
  • url должен совпадать с endpoint в route handler. Если на сервере включён transformer: superjson, его нужно добавить и в httpBatchLink.

Осталось обернуть приложение в app/layout.tsx: