🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
tRPC Beispiele
tRPC Beispiele einschließlich typsicherer End-to-End-APIs, Prozeduren, Middleware, Authentifizierung und Client-Server-Integrationsmuster
💻 tRPC Grundkonfiguration typescript
🟢 simple
⭐⭐
Grundlegende tRPC Server- und Client-Konfiguration mit einfachen Prozeduren und Typsicherheit
⏱️ 30 min
🏷️ trpc, typescript, api, type-safe
Prerequisites:
TypeScript basics, Express.js, REST API concepts
// tRPC Basic Setup Example
// Server-side setup and client integration
import { initTRPC } from '@trpc/server';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
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;
💻 tRPC Middleware und Authentifizierung typescript
🟡 intermediate
⭐⭐⭐⭐
Erweiterte tRPC Muster einschließlich Middleware, Authentifizierung, Autorisierung und Fehlerbehandlung
⏱️ 45 min
🏷️ trpc, middleware, auth, security, typescript
Prerequisites:
tRPC basics, TypeScript, JWT, Express.js, Security concepts
// 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;
💻 tRPC React Integration typescript
🟡 intermediate
⭐⭐⭐⭐
tRPC Integration mit React einschließlich Hooks, Queries, Mutations, Subscriptions und TanStack Query Integration
⏱️ 50 min
🏷️ trpc, react, frontend, integration, typescript
Prerequisites:
tRPC basics, React hooks, TanStack Query, 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;