Elysia Tools

在一个清晰的工作台里浏览工具、案例、可视化和主题指南。

热门分类

专题

语言
ZHESPTRUFRDEEN

Elysia Tools

导航

API 框架

tRPC 示例

tRPC示例,包括端到端类型安全API、程序、中间件、认证和客户端-服务器集成模式

案例

这个案例集合中的条目

代码typescript简单

tRPC 基础设置

基本的tRPC服务器和客户端设置,包含简单程序和类型安全

难度

4/10

预计时间

30 min

标签

trpc, typescript, api, type-safe

前置条件

TypeScript basics, Express.js, REST API concepts

typescript
// tRPC Basic Setup Example
// Server-side setup and client integration

import { initTRPC } from '@trpc/server';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { observable } from '@trpc/server/observable';
import express from 'express';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';

// 1. Initialize tRPC
const t = initTRPC.create();

// 2. Define context type
export type Context = {
  userId?: string;
  user?: {
    id: string;
    name: string;
  };
};

// 3. Create router
export const appRouter = t.router({
  // Simple query procedure
  hello: t.procedure
    .query(() => {
      return { greeting: 'Hello tRPC!' };
    }),

  // Query with input validation
  getUser: t.procedure
    .input((val: unknown) => {
      if (typeof val === 'string' || typeof val === 'number') {
        return String(val);
      }
      throw new Error(`Invalid input: expected string or number`);
    })
    .query(({ input }) => {
      return {
        id: input,
        name: `User ${input}`,
        email: `user${input}@example.com`,
        createdAt: new Date().toISOString()
      };
    }),

  // Simple mutation
  createUser: t.procedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected object');
      }
      const { name, email } = val as { name?: unknown; email?: unknown };

      if (typeof name !== 'string' || typeof email !== 'string') {
        throw new Error('Name and email must be strings');
      }

      return { name, email };
    })
    .mutation(({ input }) => {
      // Simulate database save
      const user = {
        id: Math.random().toString(36).substr(2, 9),
        name: input.name,
        email: input.email,
        createdAt: new Date().toISOString()
      };

      return { success: true, user };
    }),

  // Query with context
  me: t.procedure
    .query(({ ctx }) => {
      return ctx.user || null;
    }),

  // Subscription example
  onHello: t.procedure
    .subscription(() => {
      return observable((emit) => {
        const timer = setInterval(() => {
          emit.next({ greeting: 'Hello from subscription!' });
        }, 1000);

        return () => {
          clearInterval(timer);
        };
      });
    }),
});

// Export router type
export type AppRouter = typeof appRouter;

// 4. Express server setup
const app = express();

app.use('/trpc', createExpressMiddleware({
  router: appRouter,
  createContext: ({ req }) => {
    // Simulate authentication
    const userId = req.headers['x-user-id'] as string;

    return {
      userId,
      user: userId ? {
        id: userId,
        name: `User ${userId}`
      } : undefined,
    };
  },
}));

const server = app.listen(3000, () => {
  console.log('tRPC server running on port 3000');
});

// 5. Client setup (can be in separate file)
const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
      headers: {
        'x-user-id': 'user123', // Example auth header
      },
    }),
  ],
});

// 6. Client usage examples
async function clientExamples() {
  // Query
  const hello = await trpc.hello.query();
  console.log(hello); // { greeting: 'Hello tRPC!' }

  // Query with input
  const user = await trpc.getUser.query('123');
  console.log(user); // { id: '123', name: 'User 123', email: '[email protected]', ... }

  // Mutation
  const newUser = await trpc.createUser.mutate({
    name: 'John Doe',
    email: '[email protected]'
  });
  console.log(newUser); // { success: true, user: { id: 'abc', name: 'John Doe', ... } }

  // Query with context
  const me = await trpc.me.query();
  console.log(me); // { id: 'user123', name: 'User user123' }

  // Subscription
  const subscription = trpc.onHello.subscribe(undefined, {
    onData(data) {
      console.log(data); // { greeting: 'Hello from subscription!' } every second
    },
    onError(err) {
      console.error('Subscription error:', err);
    },
  });

  // Clean up subscription after 5 seconds
  setTimeout(() => {
    subscription.unsubscribe();
  }, 5000);
}

// Execute client examples
clientExamples().catch(console.error);

// 7. Error handling example
const errorRouter = t.router({
  mightFail: t.procedure
    .query(() => {
      // Simulate potential error
      if (Math.random() > 0.5) {
        throw new Error('Random failure occurred!');
      }
      return { success: true };
    }),
});

// 8. Combined router with error handling
export const combinedRouter = t.router({
  app: appRouter,
  error: errorRouter,
});

export type CombinedRouter = typeof combinedRouter;

// 9. Async procedure example
const asyncRouter = t.router({
  fetchData: t.procedure
    .input((val: unknown) => {
      if (typeof val !== 'string') {
        throw new Error('Expected string URL');
      }
      return val;
    })
    .query(async ({ input }) => {
      // Simulate async operation (e.g., database query or API call)
      await new Promise(resolve => setTimeout(resolve, 1000));

      return {
        url: input,
        data: `Data fetched from ${input}`,
        timestamp: new Date().toISOString()
      };
    }),

  processItem: t.procedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected object');
      }
      return val as { id: string; data: any };
    })
    .mutation(async ({ input }) => {
      // Simulate async processing
      await new Promise(resolve => setTimeout(resolve, 500));

      return {
        processed: true,
        id: input.id,
        result: `Processed: ${JSON.stringify(input.data)}`,
        processedAt: new Date().toISOString()
      };
    }),
});

export type AsyncRouter = typeof asyncRouter;
代码typescript中等

tRPC 中间件和认证

高级tRPC模式,包括中间件、认证、授权和错误处理

难度

7/10

预计时间

45 min

标签

trpc, middleware, auth, security, typescript

前置条件

tRPC basics, TypeScript, JWT, Express.js, Security concepts

typescript
// tRPC Middleware and Authentication Example

import { initTRPC, TRPCError } from '@trpc/server';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import express from 'express';
import jwt from 'jsonwebtoken';

// 1. Initialize tRPC
const t = initTRPC.create();

// 2. Define database/user types
interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user';
}

interface Database {
  users: User[];
  posts: Array<{
    id: string;
    title: string;
    content: string;
    authorId: string;
    published: boolean;
    createdAt: Date;
  }>;
}

// Mock database
const db: Database = {
  users: [
    { id: '1', email: '[email protected]', name: 'Admin User', role: 'admin' },
    { id: '2', email: '[email protected]', name: 'Regular User', role: 'user' },
  ],
  posts: [
    { id: '1', title: 'First Post', content: 'Hello World', authorId: '1', published: true, createdAt: new Date() },
    { id: '2', title: 'Draft Post', content: 'This is a draft', authorId: '2', published: false, createdAt: new Date() },
  ],
};

// 3. Define context type
export interface Context {
  user?: User;
  req: express.Request;
}

// 4. Authentication middleware
const isAuthed = t.middleware(async ({ next, ctx }) => {
  const authHeader = ctx.req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret-key') as { userId: string };
    const user = db.users.find(u => u.id === decoded.userId);

    if (!user) {
      throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not found' });
    }

    return next({
      ctx: {
        ...ctx,
        user,
      },
    });
  } catch (error) {
    throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid token' });
  }
});

// 5. Role-based authorization middleware
const hasRole = (role: 'admin' | 'user') =>
  t.middleware(async ({ next, ctx }) => {
    if (!ctx.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    if (ctx.user.role !== role && ctx.user.role !== 'admin') {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: `Requires ${role} role or higher`
      });
    }

    return next({
      ctx: {
        ...ctx,
      },
    });
  });

// 6. Logging middleware
const logger = t.middleware(async ({ next, path, type, input }) => {
  const start = Date.now();
  const result = await next({ ctx: {} });
  const duration = Date.now() - start;

  console.log(`[${new Date().toISOString()}] ${type.toUpperCase()} ${path}: ${duration}ms`);

  return result;
});

// 7. Error handling middleware
const errorHandler = t.middleware(async ({ next }) => {
  try {
    return await next();
  } catch (error) {
    console.error('tRPC Error:', error);
    throw error;
  }
});

// 8. Rate limiting middleware
const rateLimit = t.middleware(async ({ next, ctx }) => {
  const clientIP = ctx.req.ip;
  const currentTime = Date.now();

  // In production, you'd use Redis or a proper rate limiting store
  const requestCounts = new Map<string, { count: number; resetTime: number }>();

  const existing = requestCounts.get(clientIP);

  if (existing && currentTime < existing.resetTime) {
    if (existing.count >= 10) { // 10 requests per minute
      throw new TRPCError({
        code: 'TOO_MANY_REQUESTS',
        message: 'Rate limit exceeded. Please try again later.'
      });
    }
    existing.count++;
  } else {
    requestCounts.set(clientIP, {
      count: 1,
      resetTime: currentTime + 60000, // 1 minute
    });
  }

  return next();
});

// 9. Protected procedure creator
const protectedProcedure = t.procedure
  .use(errorHandler)
  .use(logger)
  .use(rateLimit)
  .use(isAuthed);

const adminProcedure = protectedProcedure.use(hasRole('admin'));

// 10. Create router with middleware
export const authRouter = t.router({
  // Public procedures
  login: t.procedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected credentials object');
      }
      return val as { email: string; password: string };
    })
    .mutation(({ input }) => {
      // Simulate authentication
      const user = db.users.find(u => u.email === input.email);

      if (!user || input.password !== 'password') {
        throw new TRPCError({
          code: 'UNAUTHORIZED',
          message: 'Invalid credentials'
        });
      }

      const token = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET || 'secret-key',
        { expiresIn: '24h' }
      );

      return { token, user };
    }),

  // Protected procedures
  getProfile: protectedProcedure
    .query(({ ctx }) => {
      return ctx.user;
    }),

  updateProfile: protectedProcedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected profile data');
      }
      return val as { name?: string; email?: string };
    })
    .mutation(({ input, ctx }) => {
      if (!ctx.user) {
        throw new TRPCError({ code: 'UNAUTHORIZED' });
      }

      const userIndex = db.users.findIndex(u => u.id === ctx.user!.id);
      if (userIndex === -1) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
      }

      // Update user
      const updatedUser = { ...db.users[userIndex] };
      if (input.name) updatedUser.name = input.name;
      if (input.email) updatedUser.email = input.email;

      db.users[userIndex] = updatedUser;

      return updatedUser;
    }),

  // Admin-only procedures
  getUsers: adminProcedure
    .query(() => {
      return db.users;
    }),

  deleteUser: adminProcedure
    .input((val: unknown) => {
      if (typeof val !== 'string') {
        throw new Error('Expected user ID string');
      }
      return val;
    })
    .mutation(({ input }) => {
      const userIndex = db.users.findIndex(u => u.id === input);
      if (userIndex === -1) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
      }

      const deletedUser = db.users[userIndex];
      db.users.splice(userIndex, 1);

      return { success: true, deletedUser };
    }),

  // Post management with ownership validation
  createPost: protectedProcedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected post data');
      }
      const { title, content } = val as { title?: unknown; content?: unknown };

      if (typeof title !== 'string' || typeof content !== 'string') {
        throw new Error('Title and content must be strings');
      }

      return { title, content };
    })
    .mutation(({ input, ctx }) => {
      const post = {
        id: Math.random().toString(36).substr(2, 9),
        title: input.title,
        content: input.content,
        authorId: ctx.user!.id,
        published: false,
        createdAt: new Date(),
      };

      db.posts.push(post);
      return post;
    }),

  // Procedure with multiple middleware
  getSensitiveData: protectedProcedure
    .use(logger)
    .use(rateLimit)
    .query(({ ctx }) => {
      // This procedure uses both auth middleware and additional middleware
      return {
        user: ctx.user,
        data: 'This is sensitive data only accessible to authenticated users',
        timestamp: new Date().toISOString(),
      };
    }),

  // Custom error handling
  riskyOperation: protectedProcedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected config object');
      }
      return val as { shouldFail: boolean };
    })
    .mutation(({ input }) => {
      if (input.shouldFail) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'The operation failed as requested',
          cause: new Error('Simulated failure')
        });
      }

      return { success: true, message: 'Operation completed successfully' };
    }),
});

// 11. Validation middleware example
const validateInput = <T>(validator: (input: unknown) => T) =>
  t.middleware(async ({ next, rawInput }) => {
    if (!rawInput) {
      throw new TRPCError({ code: 'BAD_REQUEST', message: 'Input is required' });
    }

    try {
      const parsed = validator(rawInput);
      return next({ ctx: { parsedInput: parsed } });
    } catch (error) {
      throw new TRPCError({
        code: 'BAD_REQUEST',
        message: `Validation error: ${error.message}`
      });
    }
  });

// Example with validation middleware
const validatedRouter = t.router({
  createUserWithValidation: t.procedure
    .input((val: unknown) => {
      if (typeof val !== 'object' || val === null) {
        throw new Error('Expected user object');
      }
      const user = val as { name: unknown; email: unknown; age: unknown };

      if (typeof user.name !== 'string' || user.name.length < 2) {
        throw new Error('Name must be at least 2 characters');
      }

      if (typeof user.email !== 'string' || !user.email.includes('@')) {
        throw new Error('Valid email is required');
      }

      if (typeof user.age !== 'number' || user.age < 18) {
        throw new Error('Age must be at least 18');
      }

      return user as { name: string; email: string; age: number };
    })
    .use(validateInput((input: unknown) => {
      // Additional validation logic here
      return input as { name: string; email: string; age: number };
    }))
    .mutation(({ ctx }) => {
      const { parsedInput } = ctx as { parsedInput: { name: string; email: string; age: number } };

      const newUser: User = {
        id: Math.random().toString(36).substr(2, 9),
        email: parsedInput.email,
        name: parsedInput.name,
        role: 'user'
      };

      db.users.push(newUser);
      return { success: true, user: newUser };
    }),
});

// 12. Combined router
export const combinedRouter = t.router({
  auth: authRouter,
  validated: validatedRouter,
});

export type AppRouter = typeof combinedRouter;

// 13. Express server setup with middleware
const app = express();
app.use(express.json());

app.use('/trpc', createExpressMiddleware({
  router: combinedRouter,
  createContext: ({ req }) => ({ req }),
  onError: ({ error, type, path, input, ctx, req }) => {
    // Global error handling
    console.error(`tRPC Error on ${type.toUpperCase()} ${path}:`, error);

    if (error.code === 'INTERNAL_SERVER_ERROR') {
      // Log internal errors
      console.error('Internal error details:', error.cause);
    }
  },
}));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`tRPC server with middleware running on port ${PORT}`);
});

export default app;
代码typescript中等

tRPC React 集成

tRPC与React的集成,包括hooks、查询、变更、订阅和TanStack Query集成

难度

8/10

预计时间

50 min

标签

trpc, react, frontend, integration, typescript

前置条件

tRPC basics, React hooks, TanStack Query, TypeScript

typescript
// tRPC React Integration Example
// Frontend React integration with tRPC and TanStack Query

import React, { useState } from 'react';
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// 1. tRPC React Query setup
export const trpc = createTRPCReact<AppRouter>();

// 2. Create tRPC client
const getTRPCClient = () => {
  return trpc.createClient({
    links: [
      httpBatchLink({
        url: 'http://localhost:3000/trpc',
        headers: () => {
          const token = localStorage.getItem('auth-token');
          return token ? { Authorization: `Bearer ${token}` } : {};
        },
      }),
    ],
  });
};

// 3. Query Client setup
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      retry: (failureCount, error) => {
        // Don't retry on 4xx errors
        if (error.data?.code === 'UNAUTHORIZED' || error.data?.code === 'FORBIDDEN') {
          return false;
        }
        return failureCount < 3;
      },
    },
  },
});

// 4. App wrapper component
export function App() {
  const [trpcClient] = useState(() => getTRPCClient());

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <div className="app">
          <Navigation />
          <Routes />
        </div>
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// 5. Navigation component
function Navigation() {
  const utils = trpc.useContext();

  const handleLogout = () => {
    localStorage.removeItem('auth-token');
    // Invalidate all queries when logging out
    utils.auth.getProfile.invalidate();
  };

  return (
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/profile">Profile</a></li>
        <li><a href="/posts">Posts</a></li>
      </ul>
      <button onClick={handleLogout}>Logout</button>
    </nav>
  );
}

// 6. Authentication component
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const login = trpc.auth.login.useMutation({
    onSuccess: (data) => {
      localStorage.setItem('auth-token', data.token);
      // Invalidate and refetch user profile
      trpc.auth.getProfile.invalidate();
    },
    onError: (error) => {
      alert(`Login failed: ${error.message}`);
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    login.mutate({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>
      <div>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <button type="submit" disabled={login.isLoading}>
        {login.isLoading ? 'Logging in...' : 'Login'}
      </button>
      {login.error && <p className="error">{login.error.message}</p>}
    </form>
  );
}

// 7. Profile component with tRPC hooks
function Profile() {
  const { data: user, isLoading, error } = trpc.auth.getProfile.useQuery();
  const utils = trpc.useContext();

  const [isEditing, setIsEditing] = useState(false);
  const [editName, setEditName] = useState(user?.name || '');

  const updateProfile = trpc.auth.updateProfile.useMutation({
    onSuccess: () => {
      setIsEditing(false);
      // Invalidate to refetch updated data
      utils.auth.getProfile.invalidate();
    },
  });

  const handleSave = () => {
    updateProfile.mutate({ name: editName });
  };

  if (isLoading) return <div>Loading profile...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>Please log in to view your profile</div>;

  return (
    <div className="profile">
      <h2>Profile</h2>
      <div>
        <strong>Email:</strong> {user.email}
      </div>
      <div>
        <strong>Role:</strong> {user.role}
      </div>
      <div>
        <strong>Name:</strong>
        {isEditing ? (
          <>
            <input
              type="text"
              value={editName}
              onChange={(e) => setEditName(e.target.value)}
            />
            <button onClick={handleSave} disabled={updateProfile.isLoading}>
              {updateProfile.isLoading ? 'Saving...' : 'Save'}
            </button>
            <button onClick={() => setIsEditing(false)}>Cancel</button>
          </>
        ) : (
          <>
            {user.name}
            <button onClick={() => {
              setEditName(user.name);
              setIsEditing(true);
            }}>
              Edit
            </button>
          </>
        )}
      </div>
      {updateProfile.error && (
        <p className="error">{updateProfile.error.message}</p>
      )}
    </div>
  );
}

// 8. Posts management component
function PostsManagement() {
  const { data: posts, isLoading } = trpc.auth.createPost.useQuery(undefined, {
    // Custom query key to fetch posts
    queryKey: ['posts'],
    // This is a workaround since we don't have a dedicated getPosts procedure
  });

  const createPost = trpc.auth.createPost.useMutation();
  const utils = trpc.useContext();

  const [newPost, setNewPost] = useState({ title: '', content: '' });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    createPost.mutate(newPost, {
      onSuccess: () => {
        setNewPost({ title: '', content: '' });
        // Invalidate posts list
        utils.auth.createPost.invalidate();
      },
    });
  };

  if (isLoading) return <div>Loading posts...</div>;

  return (
    <div className="posts">
      <h2>Create Post</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            placeholder="Title"
            value={newPost.title}
            onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
            required
          />
        </div>
        <div>
          <textarea
            placeholder="Content"
            value={newPost.content}
            onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
            required
          />
        </div>
        <button type="submit" disabled={createPost.isLoading}>
          {createPost.isLoading ? 'Creating...' : 'Create Post'}
        </button>
        {createPost.error && (
          <p className="error">{createPost.error.message}</p>
        )}
      </form>

      <h3>Posts</h3>
      {posts && (
        <div className="posts-list">
          {/* Render posts here */}
          <p>Posts would be rendered here with actual data</p>
        </div>
      )}
    </div>
  );
}

// 9. Data fetching with custom hooks
function useUserData() {
  const { data: user, isLoading: userLoading } = trpc.auth.getProfile.useQuery();
  const { data: posts, isLoading: postsLoading } = trpc.auth.createPost.useQuery(undefined, {
    queryKey: ['user-posts'],
  });

  return {
    user,
    posts,
    isLoading: userLoading || postsLoading,
  };
}

function UserDashboard() {
  const { user, posts, isLoading } = useUserData();

  if (isLoading) return <div>Loading dashboard...</div>;
  if (!user) return <div>Please log in</div>;

  return (
    <div className="dashboard">
      <h2>Welcome, {user.name}!</h2>
      <p>You have {posts?.length || 0} posts</p>

      <div className="stats">
        <div className="stat-card">
          <h3>{user.role === 'admin' ? 'Admin Dashboard' : 'User Dashboard'}</h3>
          <p>Role: {user.role}</p>
        </div>
      </div>
    </div>
  );
}

// 10. Optimistic updates example
function TodoList() {
  const { data: todos = [] } = trpc.todos.list.useQuery();
  const utils = trpc.useContext();

  const addTodo = trpc.todos.add.useMutation({
    onMutate: async (newTodo) => {
      // Cancel any outgoing refetches
      await utils.todos.list.cancel();

      // Snapshot the previous value
      const previousTodos = utils.todos.list.getData();

      // Optimistically update to the new value
      utils.todos.list.setData(undefined, (old) => [
        ...(old || []),
        { ...newTodo, id: 'temp-' + Date.now(), completed: false }
      ]);

      // Return context with the previous value
      return { previousTodos };
    },
    onError: (err, newTodo, context) => {
      // If the mutation fails, use the context returned from onMutate
      utils.todos.list.setData(undefined, context?.previousTodos);
    },
    onSettled: () => {
      // Always refetch after error or success
      utils.todos.list.invalidate();
    },
  });

  return (
    <div>
      <h2>Todos</h2>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <button
        onClick={() => addTodo.mutate({ title: 'New Todo' })}
        disabled={addTodo.isLoading}
      >
        Add Todo
      </button>
    </div>
  );
}

// 11. Subscription component
function RealTimeData() {
  const { data, error, isLoading } = trpc.onHello.useSubscription(undefined, {
    onData(data) {
      console.log('Received data:', data);
    },
    onError(err) {
      console.error('Subscription error:', err);
    },
  });

  if (isLoading) return <div>Connecting to real-time updates...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h2>Real-time Updates</h2>
      <p>Latest message: {data?.greeting}</p>
    </div>
  );
}

// 12. Error boundary for tRPC errors
class TrpcErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error?: Error }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('tRPC Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: undefined })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 13. Main app routes
function Routes() {
  const [route, setRoute] = useState('/');

  const renderRoute = () => {
    switch (route) {
      case '/':
        return <UserDashboard />;
      case '/profile':
        return <Profile />;
      case '/posts':
        return <PostsManagement />;
      case '/todos':
        return <TodoList />;
      case '/realtime':
        return <RealTimeData />;
      default:
        return <LoginForm />;
    }
  };

  return (
    <TrpcErrorBoundary>
      <div>
        <nav style={{ marginBottom: '20px' }}>
          <button onClick={() => setRoute('/')}>Dashboard</button>
          <button onClick={() => setRoute('/profile')}>Profile</button>
          <button onClick={() => setRoute('/posts')}>Posts</button>
          <button onClick={() => setRoute('/todos')}>Todos</button>
          <button onClick={() => setRoute('/realtime')}>Real-time</button>
        </nav>
        {renderRoute()}
      </div>
    </TrpcErrorBoundary>
  );
}

// 14. Custom hook for offline support
function useOfflineSupport() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  React.useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Usage example
function AppWithOfflineSupport() {
  const isOnline = useOfflineSupport();

  return (
    <div className={isOnline ? 'online' : 'offline'}>
      {!isOnline && <div className="offline-warning">You are offline</div>}
      <App />
    </div>
  );
}

export default AppWithOfflineSupport;

工具

经常与该案例搭配使用的工具

相关内容

相关案例集合

工具使用指南

帮助搜索与理解工作流的案例说明

标题

tRPC 示例

描述

tRPC示例,包括端到端类型安全API、程序、中间件、认证和客户端-服务器集成模式

概览

tRPC示例,包括端到端类型安全API、程序、中间件、认证和客户端-服务器集成模式

适用场景

  • Use this sample set when you want reference inputs for tRPC 示例.
  • Use it to preview output formats and compare related sample variants.

使用方式

  1. 1Open a sample entry to inspect the prepared example payload or asset.
  2. 2Compare it with related tools to understand how the workflow connects.

常见用途

  • tRPC 基础设置
  • tRPC 中间件和认证
  • tRPC React 集成

常见问题

What is included in tRPC 示例?

This page includes curated sample entries, related tools, and neighboring sample collections for tRPC 示例.

首页
工具3197
案例593
可视化461
文件查看器103
专题229
标签333
图像、音频与视频655

图像、音频、视频处理、转换与优化工具

数学、日期与金融644

计算器、数值计算、日期逻辑、统计与金融工具

设计与颜色296

颜色、版式、图形、视觉样式与设计辅助工具

文本与写作209

写作、清洗、格式化、提取与文本分析工具

开发与 Web193

开发者工具、网络处理、Web 调试与自动化辅助工具

转换与编码175

格式、文件、压缩包、单位与编码转换工具

界面截图结构化、人工校对与开发交付
钢琴旋律草稿创作、MIDI 核验与试听样稿交付
复古游戏纹理制作、候选评审与素材交付
操作截图到可复核图文指南与培训交付
网页改版响应式视觉回归审核与证据交付

© 2026 Elysia Tools. 保留所有权利。

文档价格关于我们服务条款隐私政策Cookie 政策联系我们

Development

JSON 转 TypeScript

将 JSON 数据转换为 TypeScript interface 或 type,支持联合类型、可选字段与嵌套对象推断

Web 框架

Elysia.js 示例

Elysia.js 现代TypeScript Web框架示例,包括Hello World、路由、验证和插件开发

Web 框架

NestJS 示例

NestJS企业级Node.js框架示例,包括模块、控制器、服务和部署