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