Modern React Patterns for Production Applications
Modern React Patterns for Production Applications
After years of building and maintaining large-scale React applications, I've learned that the difference between a good app and a great one often lies in the patterns and practices we choose. Here are some of the most effective patterns I use in production applications.
1. Compound Components Pattern
The Compound Components pattern provides a flexible and expressive way to build complex components while maintaining a clean API:
// A flexible and reusable select component
const Select = {
Root: ({ children, value, onChange }: SelectRootProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<SelectContext.Provider value={{ isOpen, setIsOpen, value, onChange }}>
<div className="relative">{children}</div>
</SelectContext.Provider>
);
},
Trigger: ({ children }: { children: React.ReactNode }) => {
const { isOpen, setIsOpen, value } = useSelectContext();
return (
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 border rounded-md"
>
{children}
<ChevronDown className={cn("h-4 w-4 transition-transform", {
"transform rotate-180": isOpen
})} />
</button>
);
},
Content: ({ children }: { children: React.ReactNode }) => {
const { isOpen } = useSelectContext();
if (!isOpen) return null;
return (
<div className="absolute top-full mt-1 w-full border rounded-md bg-background shadow-lg">
{children}
</div>
);
},
Item: ({ value, children }: SelectItemProps) => {
const { onChange, setIsOpen } = useSelectContext();
return (
<button
onClick={() => {
onChange(value);
setIsOpen(false);
}}
className="w-full px-3 py-2 text-left hover:bg-accent transition-colors"
>
{children}
</button>
);
}
};
// Usage
function UserRoleSelect() {
const [role, setRole] = useState<string>('user');
return (
<Select.Root value={role} onChange={setRole}>
<Select.Trigger>
{role.charAt(0).toUpperCase() + role.slice(1)}
</Select.Trigger>
<Select.Content>
<Select.Item value="user">User</Select.Item>
<Select.Item value="admin">Admin</Select.Item>
<Select.Item value="moderator">Moderator</Select.Item>
</Select.Content>
</Select.Root>
);
}
2. Custom Hooks for Complex Logic
I often extract complex logic into custom hooks, making components cleaner and logic reusable:
function useDebounceCallback<T extends (...args: any[]) => any>(
callback: T,
delay: number
): T {
const timeoutRef = useRef<NodeJS.Timeout>();
const callbackRef = useRef(callback);
callbackRef.current = callback;
return useCallback(
((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
}) as T,
[delay]
);
}
function useIntersectionObserver(
elementRef: RefObject<Element>,
options: IntersectionObserverInit = {}
): boolean {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(element);
return () => observer.disconnect();
}, [elementRef, options]);
return isIntersecting;
}
// Usage example
function SearchInput() {
const debouncedSearch = useDebounceCallback((query: string) => {
// API call here
}, 300);
return (
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
className="px-3 py-2 border rounded-md"
/>
);
}
function LazyImage({ src, alt }: { src: string; alt: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const isVisible = useIntersectionObserver(imgRef);
return (
<img
ref={imgRef}
src={isVisible ? src : undefined}
alt={alt}
className="w-full h-full object-cover"
/>
);
}
3. Performance Optimization Patterns
Here are some patterns I use to optimize React performance:
// 1. Memoization with proper dependencies
const MemoizedExpensiveComponent = memo(
function ExpensiveComponent({ data, onAction }: Props) {
return (
// Component implementation
);
},
(prevProps, nextProps) => {
return (
prevProps.data.id === nextProps.data.id &&
prevProps.onAction === nextProps.onAction
);
}
);
// 2. Virtualized Lists for Large Datasets
function VirtualizedList({ items }: { items: Item[] }) {
return (
<VirtualList
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</VirtualList>
);
}
// 3. Lazy Loading with Suspense
const LazyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense
fallback={
<div className="flex items-center justify-center min-h-screen">
<Spinner />
</div>
}
>
<LazyComponent />
</Suspense>
);
}
4. Error Boundaries with Recovery UI
I always implement error boundaries with recovery options:
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log to error reporting service
console.error('Error boundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="p-4 rounded-lg bg-destructive/10 text-destructive">
<h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
<p className="mb-4">{this.state.error?.message}</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
5. State Management Patterns
Here's how I organize application state:
// 1. Context + Reducer for Complex State
interface State {
user: User | null;
theme: 'light' | 'dark';
notifications: Notification[];
}
type Action =
| { type: 'SET_USER'; payload: User | null }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'ADD_NOTIFICATION'; payload: Notification }
| { type: 'REMOVE_NOTIFICATION'; payload: string };
const AppContext = createContext<{
state: State;
dispatch: Dispatch<Action>;
} | null>(null);
// 2. Custom Hook for State Management
function useAppState() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context;
}
// 3. Selector Pattern to Prevent Unnecessary Rerenders
function useUser() {
const { state } = useAppState();
return state.user;
}
function useTheme() {
const { state } = useAppState();
return state.theme;
}
// Usage
function UserProfile() {
const user = useUser();
const { dispatch } = useAppState();
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<button
onClick={() => dispatch({ type: 'SET_USER', payload: null })}
>
Logout
</button>
</div>
);
}
Best Practices for Production
- Component Organization:
// Feature-based structure
src/
features/
auth/
components/
hooks/
utils/
types.ts
index.ts
dashboard/
components/
hooks/
utils/
types.ts
index.ts
- Testing Strategy:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('user can submit form with valid data', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(
screen.getByLabelText(/email/i),
'test@example.com'
);
await userEvent.type(
screen.getByLabelText(/password/i),
'password123'
);
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
Conclusion
These patterns have proven invaluable in building robust React applications. They help maintain code quality, improve performance, and make the codebase more maintainable as it grows.
Key takeaways:
- Use compound components for flexible, reusable UI components
- Extract complex logic into custom hooks
- Implement proper performance optimizations
- Handle errors gracefully with error boundaries
- Organize state management based on application needs
Remember, patterns are tools - choose them based on your specific requirements and constraints. Stay tuned for more deep dives into React patterns and best practices!