🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples tRPC
Exemples tRPC incluant les API de type sécurisé de bout en bout, procédures, middleware, authentification et patrons d'intégration client-serveur
💻 Configuration de Base tRPC typescript
🟢 simple
⭐⭐
Configuration de base serveur et client tRPC avec procédures simples et sécurité de type
⏱️ 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;
💻 Middleware et Authentification tRPC typescript
🟡 intermediate
⭐⭐⭐⭐
Patrons avancés tRPC incluant middleware, authentification, autorisation et gestion d'erreurs
⏱️ 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;
💻 Intégration tRPC React typescript
🟡 intermediate
⭐⭐⭐⭐
Intégration tRPC avec React incluant hooks, queries, mutations, suscriptions et intégration TanStack Query
⏱️ 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;