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

  1. Use prefetching - Prefetch data on the server for better performance
  2. Handle loading states - Always show loading indicators
  3. Handle errors - Provide user-friendly error messages
  4. Use optimistic updates - Update UI immediately for better UX
  5. Invalidate queries - Invalidate related queries after mutations
  6. Type safety - Leverage TypeScript inference for type safety