5.1 安装与配置
pnpm add @trpc/react-query @tanstack/react-query
pnpm add @trpc/client @trpc/server
SHELL
创建 tRPC 客户端单例
// lib/trpc.ts — 客户端 tRPC 实例
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/router';
// 注意:只导入类型,不导入运行时代码
export const trpc = createTRPCReact<AppRouter>();
TS
5.2 TRPCProvider:包裹应用
// components/providers.tsx
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, loggerLink } from '@trpc/client';
import { trpc } from '@/lib/trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 分钟内不重新请求
retry: false, // 失败不自动重试(tRPC 错误一般不需要)
},
},
}));
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
// 开发环境打印请求日志
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
// HTTP 批量链接(默认,合并多个请求为一个 HTTP 请求)
httpBatchLink({
url: '/api/trpc',
headers: () => {
const token = getAuthToken();
return token ? { Authorization: `Bearer ${token}` } : {};
},
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
TSX
5.3 useQuery:查询数据
'use client';
import { trpc } from '@/lib/trpc';
export function PostList() {
// 基础查询
const { data, isLoading, error, refetch } =
trpc.post.list.useQuery({ page: 1, pageSize: 20 });
// ↑ 类型自动推断,错误的参数 TypeScript 会报错
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误:{error.message}</div>;
return (
<ul>
{data?.map(post => (
<li key={post.id}>{post.title}</li
{/* post.title 类型完全推断,IDE 有自动补全 */}
))}
</ul>
);
}
TSX
useQuery 高级选项
// 条件查询(enabled)
const { data: user } = trpc.user.getById.useQuery(
{ id: userId },
{
enabled: !!userId, // userId 为空时不发请求
staleTime: 5 * 60000, // 5 分钟内不重新请求
refetchOnWindowFocus: false,
select: (data) => data.name, // 只订阅 name 字段变化
}
);
// 分页查询
const [page, setPage] = useState(1);
const { data } = trpc.post.list.useQuery({ page, pageSize: 10 }, {
placeholderData: keepPreviousData, // 翻页时保持上页数据,避免闪烁
});
TSX
5.4 useMutation:变更数据
'use client';
import { trpc } from '@/lib/trpc';
import { useQueryClient } from '@tanstack/react-query';
import { getQueryKey } from '@trpc/react-query';
export function CreatePostForm() {
const queryClient = useQueryClient();
const createPost = trpc.post.create.useMutation({
onSuccess(newPost) {
// 创建成功后,使文章列表缓存失效,触发重新请求
const listKey = getQueryKey(trpc.post.list);
queryClient.invalidateQueries({ queryKey: listKey });
},
onError(error) {
// error 类型完全推断
console.error(error.message);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createPost.mutate({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="标题" />
<textarea name="content" placeholder="内容" />
<button type="submit" disabled={createPost.isPending}>
{createPost.isPending ? '提交中...' : '发布文章'}
</button>
{createPost.error && <p>{createPost.error.message}</p>}
</form>
);
}
TSX
5.5 乐观更新(Optimistic Updates)
乐观更新在请求发出前就立即更新 UI,请求成功后确认,失败时回滚。这让操作感觉即时响应,提升用户体验。
export function TodoList() {
const queryClient = useQueryClient();
const listKey = getQueryKey(trpc.todo.list, undefined, 'query');
const toggleTodo = trpc.todo.toggle.useMutation({
async onMutate(variables) {
// 取消进行中的请求,避免与乐观更新冲突
await queryClient.cancelQueries({ queryKey: listKey });
// 保存当前数据,用于回滚
const previousTodos = queryClient.getQueryData(listKey);
// 乐观地更新缓存
queryClient.setQueryData(listKey, (old: Todo[]) =>
old.map(todo =>
todo.id === variables.id
? { ...todo, completed: !todo.completed }
: todo
)
);
return { previousTodos }; // 传给 onError 用于回滚
},
onError(err, variables, context) {
// 请求失败:回滚到操作前的状态
queryClient.setQueryData(listKey, context?.previousTodos);
},
onSettled() {
// 无论成功/失败,都重新请求一次确保数据最新
queryClient.invalidateQueries({ queryKey: listKey });
},
});
// ... 渲染 Todo 列表
}
TSX
5.6 完整 CRUD 页面实战
'use client';
import { useState } from 'react';
import { trpc } from '@/lib/trpc';
import { getQueryKey } from '@trpc/react-query';
import { useQueryClient } from '@tanstack/react-query';
export default function PostsPage() {
const queryClient = useQueryClient();
const [newTitle, setNewTitle] = useState('');
// 读取列表
const { data: posts, isLoading } = trpc.post.list.useQuery({ page: 1 });
const listKey = getQueryKey(trpc.post.list);
// 创建
const createMutation = trpc.post.create.useMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: listKey });
setNewTitle('');
},
});
// 删除
const deleteMutation = trpc.post.delete.useMutation({
onSuccess: () => queryClient.invalidateQueries({ queryKey: listKey }),
});
if (isLoading) return <p>加载中...</p>;
return (
<div>
{/* 创建表单 */}
<form onSubmit={(e) => {
e.preventDefault();
createMutation.mutate({ title: newTitle, content: '内容...' });
}}>
<input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="新文章标题"
/>
<button type="submit" disabled={createMutation.isPending}>
发布
</button>
</form>
{/* 列表 */}
{posts?.map((post) => (
<div key={post.id}>
<span>{post.title}</span>
<button
onClick={() => deleteMutation.mutate({ id: post.id })}
disabled={deleteMutation.isPending}
>
删除
</button>
</div>
))}
</div>
);
}
TSX
本章小结createTRPCReact<AppRouter>() 创建类型安全的 React hooks。useQuery() 获取数据,useMutation() 执行写操作,两者均完整继承 TanStack Query 的缓存、后台刷新、错误状态能力。乐观更新通过 onMutate → onError(回滚)→ onSettled(刷新) 三步实现。getQueryKey(trpc.xxx) 获取标准化的缓存 key,用于 invalidateQueries。