React Common Patterns
Essential React patterns and design principles that help you write clean, maintainable, and scalable code:
Compound Components Pattern
Compound components are components that work together to form a complete UI. They share an implicit state and communicate with each other, providing a flexible and expressive API.
import { createContext, useContext, useState } from 'react';
// Context for sharing state
const TabsContext = createContext();
// Parent component
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Tab list component
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
// Individual tab
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
const isActive = activeTab === value;
return (
<button
onClick={() => setActiveTab(value)}
className={isActive ? 'tab active' : 'tab'}
>
{children}
</button>
);
}
// Tab panel
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return <div className="tab-panel">{children}</div>;
}
// Usage - expressive and flexible API
function App() {
return (
<Tabs defaultValue="account">
<TabList>
<Tab value="account">Account</Tab>
<Tab value="settings">Settings</Tab>
<Tab value="notifications">Notifications</Tab>
</TabList>
<TabPanel value="account">
<AccountSettings />
</TabPanel>
<TabPanel value="settings">
<GeneralSettings />
</TabPanel>
<TabPanel value="notifications">
<NotificationSettings />
</TabPanel>
</Tabs>
);
}
// Export as compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;Render Props Pattern
Render props is a technique for sharing code between components using a prop whose value is a function. It allows you to pass rendering logic from parent to child.
// Mouse tracker component using render props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position);
}
// Data fetcher with render props
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
}
// Usage
function App() {
return (
<div>
{/* Mouse position display */}
<MouseTracker
render={({ x, y }) => (
<div>Mouse position: {x}, {y}</div>
)}
/>
{/* User data display */}
<DataFetcher
url="/api/user"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>User: {data.name}</div>;
}}
/>
</div>
);
}Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new enhanced component. They are used to reuse component logic across multiple components.
// withAuth HOC - adds authentication logic
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// withLogging HOC - adds logging
function withLogging(WrappedComponent, componentName) {
return function LoggingComponent(props) {
useEffect(() => {
console.log(`${componentName} mounted`);
return () => console.log(`${componentName} unmounted`);
}, []);
return <WrappedComponent {...props} />;
};
}
// withData HOC - adds data fetching
function withData(WrappedComponent, fetchData) {
return function DataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData(props)
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [props.id]); // Refetch when ID changes
return <WrappedComponent {...props} data={data} loading={loading} error={error} />;
};
}
// Usage - compose multiple HOCs
const UserProfile = ({ user, data }) => (
<div>
<h1>{user.name}</h1>
<p>{data.bio}</p>
</div>
);
const EnhancedUserProfile = withAuth(
withLogging(
withData(UserProfile, ({ id }) => fetch(`/api/users/${id}`)),
'UserProfile'
)
);Container/Presentational Pattern
Separate components into containers (logic) and presentational (UI) components for better reusability and testability.
// Presentational component - pure UI
function UserCard({ name, email, avatar, onEdit, onDelete }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
<div>
<button onClick={onEdit}>Edit</button>
<button onClick={onDelete}>Delete</button>
</div>
</div>
);
}
// Container component - handles logic and state
function UserCardContainer({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId)
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
const handleEdit = () => {
// Edit logic
console.log('Edit user:', userId);
};
const handleDelete = async () => {
await deleteUser(userId);
// Update state or navigate
};
if (loading) return <div>Loading...</div>;
return (
<UserCard
name={user.name}
email={user.email}
avatar={user.avatar}
onEdit={handleEdit}
onDelete={handleDelete}
/>
);
}Custom Hooks Pattern
Custom hooks extract reusable logic from components. They follow the naming convention of starting with "use" and can call other hooks.
// useToggle - manage boolean state
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, { toggle, setTrue, setFalse }];
}
// useDebounce - debounce a value
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// useMediaQuery - responsive design
function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// Usage
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const isMobile = useMediaQuery('(max-width: 768px)');
const [isOpen, { toggle, setFalse }] = useToggle();
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={isMobile ? 'Search...' : 'Search for anything...'}
/>
<button onClick={toggle}>
{isOpen ? 'Close Filters' : 'Open Filters'}
</button>
{isOpen && <FilterPanel onClose={setFalse} />}
</div>
);
}Provider Pattern
The Provider pattern uses React Context to provide data to deeply nested components without prop drilling.
import { createContext, useContext, useState } from 'react';
// Create context
const AuthContext = createContext();
// Provider component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in
checkAuth()
.then(userData => {
setUser(userData);
setLoading(false);
})
.catch(() => {
setUser(null);
setLoading(false);
});
}, []);
const login = async (credentials) => {
const userData = await authenticate(credentials);
setUser(userData);
localStorage.setItem('token', userData.token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for consuming context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage in app
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
</Routes>
</Router>
</AuthProvider>
);
}
// Protected route component
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) return <div>Loading...</div>;
return isAuthenticated ? children : <Navigate to="/login" />;
}Controlled vs Uncontrolled Components
Controlled components have their state controlled by React, while uncontrolled components manage their own state internally.
// Controlled component - React controls the state
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Controlled input"
/>
);
}
// Uncontrolled component - DOM controls the state
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => {
console.log('Input value:', inputRef.current.value);
};
return (
<div>
<input
ref={inputRef}
defaultValue=""
placeholder="Uncontrolled input"
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
// Hybrid approach - controlled with default value
function HybridInput({ defaultValue, onChange }) {
const [value, setValue] = useState(defaultValue);
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
onChange?.(newValue);
};
return (
<input
value={value}
onChange={handleChange}
placeholder="Hybrid input"
/>
);
}Prop Drilling Solution - Composition
Instead of passing props through many layers, use component composition to pass components directly where they're needed.
// ❌ Prop drilling - passing props through many layers
function App() {
const user = useUser();
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name}</div>;
}
// ✅ Component composition - pass components as props
function App() {
const user = useUser();
return (
<Layout
sidebar={<Sidebar userMenu={<UserMenu user={user} />} />}
>
<MainContent />
</Layout>
);
}
function Layout({ sidebar, children }) {
return (
<div className="layout">
<div className="sidebar">{sidebar}</div>
<div className="main">{children}</div>
</div>
);
}
function Sidebar({ userMenu }) {
return (
<div>
<Navigation />
{userMenu}
</div>
);
}Lazy Loading and Code Splitting
Load components on demand to reduce initial bundle size and improve performance.
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
// Loading fallback component
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Analytics() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={analyticsData} />
</Suspense>
)}
</div>
);
}Error Boundaries
Error boundaries catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of crashing.
import { Component } from 'react';
// Error boundary class component
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// Log to error reporting service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
}
/>
</Routes>
</Router>
</ErrorBoundary>
);
}Portal Pattern
Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
import { createPortal } from 'react-dom';
// Modal component using portal
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.body // Render to body instead of parent
);
}
// Tooltip using portal
function Tooltip({ children, content, position }) {
const [isVisible, setIsVisible] = useState(false);
const [coords, setCoords] = useState({ x: 0, y: 0 });
const triggerRef = useRef();
const showTooltip = () => {
const rect = triggerRef.current.getBoundingClientRect();
setCoords({ x: rect.left, y: rect.bottom + 5 });
setIsVisible(true);
};
return (
<>
<span
ref={triggerRef}
onMouseEnter={showTooltip}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</span>
{isVisible && createPortal(
<div
className="tooltip"
style={{ position: 'absolute', left: coords.x, top: coords.y }}
>
{content}
</div>,
document.body
)}
</>
);
}
// Usage
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Modal Title</h2>
<p>Modal content here...</p>
</Modal>
<Tooltip content="Helpful information">
<span>Hover me</span>
</Tooltip>
</div>
);
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free