tRPC 客户端使用 #
客户端类型概览 #
tRPC 提供了多种客户端类型,适用于不同的使用场景:
text
┌─────────────────────────────────────────────────────────────┐
│ tRPC 客户端类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ createTRPCProxyClient │
│ ├── 基础客户端 │
│ ├── 适用于非 React 环境 │
│ └── Promise-based API │
│ │
│ createTRPCReact │
│ ├── React 专用客户端 │
│ ├── 提供 Hooks │
│ └── 集成 React Query │
│ │
│ createTRPCNext │
│ ├── Next.js 专用客户端 │
│ ├── 支持 SSR/SSG │
│ └── App Router 支持 │
│ │
│ createTRPCVue │
│ ├── Vue 专用客户端 │
│ ├── 提供 Composables │
│ └── 集成 Vue Query │
│ │
└─────────────────────────────────────────────────────────────┘
基础客户端 #
创建客户端 #
typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/routers';
export const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
});
使用客户端 #
typescript
async function main() {
const users = await client.user.list.query();
console.log(users);
const user = await client.user.getById.query({ id: '1' });
console.log(user);
const newUser = await client.user.create.mutate({
name: 'John',
email: 'john@example.com',
});
console.log(newUser);
}
main().catch(console.error);
text
┌─────────────────────────────────────────────────────────────┐
│ 客户端 API │
├─────────────────────────────────────────────────────────────┤
│ │
│ Query: │
│ client.router.procedure.query(input) │
│ │
│ Mutation: │
│ client.router.procedure.mutate(input) │
│ │
│ Subscription: │
│ client.router.procedure.subscribe(input, { │
│ onStarted: () => {}, │
│ onData: (data) => {}, │
│ onError: (err) => {}, │
│ onStopped: () => {}, │
│ }) │
│ │
└─────────────────────────────────────────────────────────────┘
React 客户端 #
安装依赖 #
bash
npm install @trpc/client @trpc/react-query @tanstack/react-query
配置客户端 #
typescript
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers';
export const trpc = createTRPCReact<AppRouter>();
配置 Provider #
tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from './trpc';
const queryClient = new QueryClient();
function App() {
const [trpcClient] = React.useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers() {
return {
Authorization: `Bearer ${getToken()}`,
};
},
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</trpc.Provider>
);
}
Query Hooks #
tsx
import { trpc } from './trpc';
function UserList() {
const usersQuery = trpc.user.list.useQuery();
if (usersQuery.isLoading) {
return <div>Loading...</div>;
}
if (usersQuery.isError) {
return <div>Error: {usersQuery.error.message}</div>;
}
return (
<ul>
{usersQuery.data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Query 选项 #
tsx
function UserDetail({ id }: { id: string }) {
const userQuery = trpc.user.getById.useQuery(
{ id },
{
enabled: !!id,
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 2,
onSuccess: (data) => {
console.log('User loaded:', data);
},
onError: (error) => {
console.error('Error:', error);
},
}
);
return <div>{userQuery.data?.name}</div>;
}
text
┌─────────────────────────────────────────────────────────────┐
│ Query 选项 │
├─────────────────────────────────────────────────────────────┤
│ │
│ enabled: boolean │
│ - 控制是否执行查询 │
│ - 适用于条件查询 │
│ │
│ staleTime: number │
│ - 数据过期时间(毫秒) │
│ - 过期内不会重新获取 │
│ │
│ refetchOnWindowFocus: boolean │
│ - 窗口聚焦时是否重新获取 │
│ │
│ retry: number | false │
│ - 失败重试次数 │
│ │
│ onSuccess: (data) => void │
│ - 成功回调 │
│ │
│ onError: (error) => void │
│ - 错误回调 │
│ │
└─────────────────────────────────────────────────────────────┘
Mutation Hooks #
tsx
function CreateUser() {
const utils = trpc.useUtils();
const createUser = trpc.user.create.useMutation({
onSuccess: (data) => {
utils.user.list.invalidate();
console.log('Created:', data);
},
onError: (error) => {
console.error('Error:', error);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createUser.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={createUser.isLoading}>
{createUser.isLoading ? 'Creating...' : 'Create'}
</button>
</form>
);
}
Mutation 状态 #
tsx
function UpdateUser({ id }: { id: string }) {
const updateUser = trpc.user.update.useMutation();
return (
<div>
<button
onClick={() => updateUser.mutate({ id, name: 'New Name' })}
disabled={updateUser.isPending}
>
{updateUser.isPending ? 'Updating...' : 'Update'}
</button>
{updateUser.isError && (
<p className="error">{updateUser.error.message}</p>
)}
{updateUser.isSuccess && (
<p className="success">Updated successfully!</p>
)}
</div>
);
}
乐观更新 #
tsx
function LikeButton({ postId }: { postId: string }) {
const utils = trpc.useUtils();
const likeMutation = trpc.post.like.useMutation({
onMutate: async (variables) => {
await utils.post.getById.cancel();
const previousPost = utils.post.getById.getData({ id: postId });
utils.post.getById.setData({ id: postId }, (old) => {
if (!old) return old;
return {
...old,
likes: old.likes + 1,
};
});
return { previousPost };
},
onError: (err, variables, context) => {
if (context?.previousPost) {
utils.post.getById.setData({ id: postId }, context.previousPost);
}
},
onSettled: () => {
utils.post.getById.invalidate({ id: postId });
},
});
return (
<button onClick={() => likeMutation.mutate({ id: postId })}>
Like
</button>
);
}
并行查询 #
tsx
function Dashboard() {
const [usersQuery, postsQuery, statsQuery] = trpc.useQueries((t) => [
t.user.list(),
t.post.list({ limit: 10 }),
t.stats.get(),
]);
if (usersQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<UsersList users={usersQuery.data} />
<PostsList posts={postsQuery.data} />
<Stats stats={statsQuery.data} />
</div>
);
}
依赖查询 #
tsx
function UserPosts({ userId }: { userId: string }) {
const userQuery = trpc.user.getById.useQuery({ id: userId });
const postsQuery = trpc.post.list.useQuery(
{ authorId: userId },
{ enabled: !!userQuery.data }
);
if (userQuery.isLoading) return <div>Loading user...</div>;
if (!userQuery.data) return <div>User not found</div>;
return (
<div>
<h1>{userQuery.data.name}'s Posts</h1>
{postsQuery.isLoading ? (
<div>Loading posts...</div>
) : (
<ul>
{postsQuery.data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</div>
);
}
无限滚动 #
tsx
function InfiniteUserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = trpc.user.infiniteList.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
return (
<div>
{data?.pages.map((page, i) => (
<Fragment key={i}>
{page.items.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading...'
: hasNextPage
? 'Load More'
: 'No More'}
</button>
</div>
);
}
Next.js 集成 #
Pages Router #
API 路由 #
typescript
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/context';
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
});
tRPC 配置 #
typescript
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers';
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
ssr: true,
});
_app.tsx #
tsx
import type { AppType } from 'next/app';
import { trpc } from '../utils/trpc';
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default trpc.withTRPC(MyApp);
App Router (Next.js 13+) #
API 路由 #
typescript
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';
import { createContext } from '@/server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
客户端 Provider #
tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './trpc';
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
Server Components #
tsx
import { serverClient } from '@/server/client';
import { UserList } from './UserList';
export default async function Page() {
const users = await serverClient.user.list();
return <UserList initialData={users} />;
}
tsx
'use client';
import { trpc } from './trpc';
export function UserList({ initialData }: { initialData: any[] }) {
const { data } = trpc.user.list.useQuery(undefined, {
initialData,
});
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Vue 集成 #
安装依赖 #
bash
npm install @trpc/client @trpc/vue @tanstack/vue-query
配置客户端 #
typescript
import { createTRPCVue } from '@trpc/vue';
import type { AppRouter } from '../server/routers';
export const trpc = createTRPCVue<AppRouter>();
配置 Provider #
typescript
import { VueQueryPlugin } from '@tanstack/vue-query';
import { trpc } from './trpc';
const app = createApp(App);
app.use(VueQueryPlugin);
app.use(trpc, {
trpcClientOptions: {
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
},
});
app.mount('#app');
使用 Composables #
vue
<script setup lang="ts">
import { trpc } from './trpc';
const { data, isLoading, error } = trpc.user.list.useQuery();
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
Mutation #
vue
<script setup lang="ts">
import { trpc } from './trpc';
const createUser = trpc.user.create.useMutation();
const handleSubmit = async (event: Event) => {
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
await createUser.mutateAsync({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" :disabled="createUser.isPending.value">
{{ createUser.isPending.value ? 'Creating...' : 'Create' }}
</button>
</form>
</template>
订阅(Subscription) #
WebSocket 配置 #
typescript
import { createTRPCProxyClient, createWSClient, wsLink } from '@trpc/client';
import type { AppRouter } from '../server/routers';
const wsClient = createWSClient({
url: 'ws://localhost:4000/trpc',
});
export const client = createTRPCProxyClient<AppRouter>({
links: [
wsLink({
client: wsClient,
}),
],
});
React 订阅 #
tsx
function NewPosts() {
const [posts, setPosts] = useState<Post[]>([]);
trpc.post.onNew.useSubscription(undefined, {
onData(post) {
setPosts((prev) => [post, ...prev]);
},
onError(err) {
console.error('Subscription error:', err);
},
});
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
混合链接 #
typescript
import {
createTRPCProxyClient,
httpBatchLink,
splitLink,
wsLink,
createWSClient,
} from '@trpc/client';
const wsClient = createWSClient({
url: 'ws://localhost:4000/trpc',
});
export const client = createTRPCProxyClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: wsLink({ client: wsClient }),
false: httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
}),
],
});
text
┌─────────────────────────────────────────────────────────────┐
│ 混合链接策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 请求类型判断: │
│ │
│ if (op.type === 'subscription') │
│ → WebSocket 链接 │
│ else │
│ → HTTP 批量链接 │
│ │
│ 优势: │
│ ✅ Query/Mutation 使用 HTTP │
│ ✅ Subscription 使用 WebSocket │
│ ✅ 按需建立长连接 │
│ │
└─────────────────────────────────────────────────────────────┘
工具函数 #
useUtils #
tsx
function DeleteUser({ id }: { id: string }) {
const utils = trpc.useUtils();
const deleteUser = trpc.user.delete.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
utils.user.getById.invalidate({ id });
},
});
return (
<button onClick={() => deleteUser.mutate({ id })}>
Delete
</button>
);
}
预取数据 #
tsx
function UserList() {
const utils = trpc.useUtils();
const handleHover = (id: string) => {
utils.user.getById.prefetch({ id });
};
const { data } = trpc.user.list.useQuery();
return (
<ul>
{data?.map((user) => (
<li
key={user.id}
onMouseEnter={() => handleHover(user.id)}
>
{user.name}
</li>
))}
</ul>
);
}
设置数据 #
tsx
function UpdateUser({ id }: { id: string }) {
const utils = trpc.useUtils();
const updateUser = trpc.user.update.useMutation({
onSuccess: (updatedUser) => {
utils.user.getById.setData({ id }, updatedUser);
utils.user.list.invalidate();
},
});
return (
<button onClick={() => updateUser.mutate({ id, name: 'New Name' })}>
Update
</button>
);
}
获取数据 #
tsx
function useUserData(id: string) {
const utils = trpc.useUtils();
return utils.user.getById.getData({ id });
}
错误处理 #
全局错误处理 #
tsx
function App() {
const [trpcClient] = React.useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ErrorBoundary
fallback={<div>Something went wrong</div>}
>
<YourApp />
</ErrorBoundary>
</QueryClientProvider>
</trpc.Provider>
);
}
局部错误处理 #
tsx
function UserList() {
const { data, error, isError } = trpc.user.list.useQuery();
if (isError) {
if (error.data?.code === 'UNAUTHORIZED') {
return <div>Please log in</div>;
}
return <div>Error: {error.message}</div>;
}
return <ul>{/* ... */}</ul>;
}
最佳实践 #
1. 组织代码 #
text
src/
├── utils/
│ └── trpc.ts # tRPC 配置
├── hooks/
│ ├── useUser.ts # 用户相关 hooks
│ └── usePost.ts # 文章相关 hooks
└── components/
└── UserList.tsx
2. 封装 Hooks #
typescript
export function useUser(id: string) {
const utils = trpc.useUtils();
const query = trpc.user.getById.useQuery({ id });
const update = trpc.user.update.useMutation({
onSuccess: (data) => {
utils.user.getById.setData({ id }, data);
},
});
const remove = trpc.user.delete.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
},
});
return {
user: query.data,
isLoading: query.isLoading,
error: query.error,
update: update.mutate,
remove: remove.mutate,
};
}
3. 类型安全 #
typescript
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '../server/routers';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type UserCreateInput = RouterInput['user']['create'];
type UserOutput = RouterOutput['user']['getById'];
下一步 #
现在你已经掌握了 tRPC 客户端的使用方法,接下来学习 类型系统,深入了解 tRPC 的类型推断机制!
最后更新:2026-03-29