General
tRPC
Usage in Frontend
Learn how to use tRPC endpoints in your React components.
tRPC provides type-safe hooks for using your API in React components. All procedures are automatically typed based on your router definitions.
Queries
Use useQuery for data fetching:
components/user-profile.tsx
'use client';
import { trpc } from '@/trpc/client';
export function UserProfile() {
const { data: user, isLoading, error } = trpc.user.getProfile.useQuery();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>Not found</div>;
return <div>Hello, {user.name}!</div>;
}Query with Input
Pass input parameters to queries:
components/post-detail.tsx
'use client';
import { trpc } from '@/trpc/client';
export function PostDetail({ postId }: { postId: string }) {
const { data: post, isLoading } = trpc.posts.getById.useQuery({ id: postId });
if (isLoading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}Conditional Queries
Enable/disable queries conditionally:
components/conditional-query.tsx
'use client';
import { trpc } from '@/trpc/client';
export function ConditionalQuery({ enabled }: { enabled: boolean }) {
const { data } = trpc.posts.list.useQuery(
{ limit: 10 },
{ enabled } // Only fetch when enabled is true
);
return <div>{/* render data */}</div>;
}Mutations
Use useMutation for data modifications:
components/create-post-form.tsx
'use client';
import { trpc } from '@/trpc/client';
import { useForm } from 'react-hook-form';
export function CreatePostForm() {
const utils = trpc.useUtils();
const { handleSubmit } = useForm<{ title: string; content: string }>();
const createPost = trpc.posts.create.useMutation({
onSuccess: () => {
// Invalidate and refetch posts list
utils.posts.list.invalidate();
}
});
const onSubmit = async (data: { title: string; content: string }) => {
try {
await createPost.mutateAsync(data);
// Handle success
} catch (error) {
// Handle error
}
};
return <form onSubmit={handleSubmit(onSubmit)}>{/* form fields */}</form>;
}Optimistic Updates
Update the UI optimistically for better UX:
components/optimistic-update.tsx
'use client';
import { trpc } from '@/trpc/client';
export function OptimisticUpdate() {
const utils = trpc.useUtils();
const updatePost = trpc.posts.update.useMutation({
onMutate: async (newData) => {
// Cancel outgoing refetches
await utils.posts.getById.cancel({ id: newData.id });
// Snapshot previous value
const previous = utils.posts.getById.getData({ id: newData.id });
// Optimistically update
utils.posts.getById.setData({ id: newData.id }, (old) => ({
...old!,
...newData
}));
return { previous };
},
onError: (err, newData, context) => {
// Rollback on error
utils.posts.getById.setData({ id: newData.id }, context?.previous);
},
onSettled: (data, error, variables) => {
// Refetch to ensure consistency
utils.posts.getById.invalidate({ id: variables.id });
}
});
return (
<button
onClick={() => updatePost.mutate({ id: '123', title: 'New Title' })}
>
Update
</button>
);
}Server-Side Usage
Prefetching in Server Components
Prefetch data on the server for better performance:
app/(saas)/dashboard/posts/page.tsx
import { HydrateClient, trpc } from '@/trpc/server';
export default async function PostsPage() {
// Prefetch data on the server
await trpc.posts.list.prefetch({ limit: 10, offset: 0 });
return (
<HydrateClient>
<PostsList />
</HydrateClient>
);
}Direct Server Calls
Call tRPC procedures directly on the server:
app/api/posts/route.ts
import { trpc } from '@/trpc/server';
export async function GET() {
const posts = await trpc.posts.list({ limit: 10, offset: 0 });
return Response.json(posts);
}Error Handling
Handle errors gracefully:
components/error-handling.tsx
'use client';
import { trpc } from '@/trpc/client';
import { TRPCClientError } from '@trpc/client';
export function ErrorHandling() {
const { data, error, isLoading } = trpc.posts.getById.useQuery(
{ id: '123' },
{
retry: (failureCount, error) => {
// Don't retry on 404
if (error.data?.code === 'NOT_FOUND') {
return false;
}
// Retry up to 3 times for other errors
return failureCount < 3;
}
}
);
if (error) {
if (error.data?.code === 'NOT_FOUND') {
return <div>Post not found</div>;
}
if (error.data?.code === 'FORBIDDEN') {
return <div>You don't have permission to view this post</div>;
}
return <div>Error: {error.message}</div>;
}
if (isLoading) return <div>Loading...</div>;
return <div>{/* render data */}</div>;
}Type Inference
Extract types from your procedures:
types/post.ts
import type { AppRouter } from '@/trpc/routers/app';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
type RouterOutputs = inferRouterOutputs<AppRouter>;
type RouterInputs = inferRouterInputs<AppRouter>;
// Extract output type
export type Post = RouterOutputs['posts']['getById'];
// Extract input type
export type CreatePostInput = RouterInputs['posts']['create'];
export type UpdatePostInput = RouterInputs['posts']['update'];Use in components:
components/typed-component.tsx
'use client';
import { trpc } from '@/trpc/client';
import type { Post } from '@/types/post';
export function TypedComponent() {
const { data: post } = trpc.posts.getById.useQuery({ id: '123' });
// post is automatically typed as Post
return <div>{post?.title}</div>;
}Query Invalidation
Invalidate queries to trigger refetches:
components/invalidation.tsx
'use client';
import { trpc } from '@/trpc/client';
export function InvalidationExample() {
const utils = trpc.useUtils();
const createPost = trpc.posts.create.useMutation();
const handleCreate = async (data: { title: string; content: string }) => {
await createPost.mutateAsync(data);
// Invalidate specific query
utils.posts.list.invalidate();
// Or invalidate all posts queries
utils.posts.invalidate();
};
return (
<button onClick={() => handleCreate({ title: 'New', content: 'Post' })}>
Create Post
</button>
);
}Best Practices
- Use prefetching - Prefetch data on the server for better performance
- Handle loading states - Always show loading indicators
- Handle errors - Provide user-friendly error messages
- Use optimistic updates - Update UI immediately for better UX
- Invalidate queries - Invalidate related queries after mutations
- Type safety - Leverage TypeScript inference for type safety