React Patterns and Best Practices
Purpose
Provides modern React patterns including:
- •Component organization
- •Hooks best practices
- •State management patterns
- •TypeScript integration
- •Performance optimization
Component Structure
typescript
// src/components/UserProfile/UserProfile.tsx
import { FC, memo } from 'react';
import { useUserProfile } from './useUserProfile';
import { UserProfileProps } from './types';
import styles from './UserProfile.module.css';
export const UserProfile: FC<UserProfileProps> = memo(({ userId }) => {
const { user, isLoading, error } = useUserProfile(userId);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null;
return (
<div className={styles.container}>
<Avatar src={user.avatar} alt={user.name} />
<h2 className={styles.name}>{user.name}</h2>
<p className={styles.email}>{user.email}</p>
</div>
);
});
UserProfile.displayName = 'UserProfile';
Custom Hooks
typescript
// src/hooks/useAsync.ts
import { useState, useCallback } from 'react';
interface AsyncState<T> {
data: T | null;
isLoading: boolean;
error: Error | null;
}
export function useAsync<T>() {
const [state, setState] = useState<AsyncState<T>>({
data: null,
isLoading: false,
error: null,
});
const execute = useCallback(async (promise: Promise<T>) => {
setState({ data: null, isLoading: true, error: null });
try {
const data = await promise;
setState({ data, isLoading: false, error: null });
return data;
} catch (error) {
setState({ data: null, isLoading: false, error: error as Error });
throw error;
}
}, []);
return { ...state, execute };
}
API Hook Pattern
typescript
// src/hooks/useApi.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../api';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: api.getUsers,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Context Pattern
typescript
// src/context/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authApi.login(credentials);
setUser(user);
};
const logout = () => {
setUser(null);
authApi.logout();
};
return (
<AuthContext.Provider value={{
user,
login,
logout,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Form Pattern (React Hook Form)
typescript
// src/components/UserForm/UserForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
});
type FormData = z.infer<typeof schema>;
export function UserForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</form>
);
}
Error Boundary
typescript
// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong</div>;
}
return this.props.children;
}
}
Performance Patterns
typescript
// Memoization
const ExpensiveComponent = memo(({ data }) => {
// Only re-renders when data changes
return <div>{/* ... */}</div>;
});
// useMemo for expensive calculations
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useCallback for stable function references
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []);
// Lazy loading
const HeavyComponent = lazy(() => import('./HeavyComponent'));
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
Key Principles
- •Composition over inheritance - Compose components
- •Single responsibility - One component, one job
- •Lift state up - Share state at common ancestor
- •Custom hooks - Extract reusable logic
- •TypeScript first - Type everything
- •Memoize wisely - Don't over-optimize
- •Error boundaries - Graceful error handling