React.js Fundamentals

React Design Patterns

18 min Lesson 35 of 40

React Design Patterns

Design patterns are proven solutions to common problems in software development. Learn the most important React patterns to write maintainable, reusable, and scalable code.

Container/Presentational Pattern

Separate logic (container) from presentation (presentational components):

// Presentational Component - Only handles UI function UserCard({ user, onEdit, onDelete }) { return ( <div className="user-card"> <img src={user.avatar} alt={user.name} /> <h3>{user.name}</h3> <p>{user.email}</p> <div className="actions"> <button onClick={() => onEdit(user.id)}>Edit</button> <button onClick={() => onDelete(user.id)}>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 = (id) => { // Edit logic console.log('Edit user', id); }; const handleDelete = (id) => { // Delete logic console.log('Delete user', id); }; if (loading) return <div>Loading...</div>; if (!user) return <div>User not found</div>; return <UserCard user={user} onEdit={handleEdit} onDelete={handleDelete} />; }
Note: With modern React hooks, this pattern is less common. Custom hooks often replace container components, but the separation of concerns principle remains valuable.

Compound Components Pattern

Create components that work together while sharing implicit state:

import { createContext, useContext, useState } from 'react'; // Context for shared state const TabsContext = createContext(); function Tabs({ children, defaultValue }) { const [activeTab, setActiveTab] = useState(defaultValue); return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs">{children}</div> </TabsContext.Provider> ); } function TabList({ children }) { return <div className="tab-list" role="tablist">{children}</div>; } function Tab({ value, children }) { const { activeTab, setActiveTab } = useContext(TabsContext); const isActive = activeTab === value; return ( <button className={`tab ${isActive ? 'active' : ''}`} onClick={() => setActiveTab(value)} role="tab" aria-selected={isActive} > {children} </button> ); } function TabPanels({ children }) { return <div className="tab-panels">{children}</div>; } function TabPanel({ value, children }) { const { activeTab } = useContext(TabsContext); if (activeTab !== value) return null; return ( <div className="tab-panel" role="tabpanel"> {children} </div> ); } // Attach sub-components to main component Tabs.List = TabList; Tabs.Tab = Tab; Tabs.Panels = TabPanels; Tabs.Panel = TabPanel; // Usage function App() { return ( <Tabs defaultValue="profile"> <Tabs.List> <Tabs.Tab value="profile">Profile</Tabs.Tab> <Tabs.Tab value="settings">Settings</Tabs.Tab> <Tabs.Tab value="notifications">Notifications</Tabs.Tab> </Tabs.List> <Tabs.Panels> <Tabs.Panel value="profile"> <h2>Profile Content</h2> </Tabs.Panel> <Tabs.Panel value="settings"> <h2>Settings Content</h2> </Tabs.Panel> <Tabs.Panel value="notifications"> <h2>Notifications Content</h2> </Tabs.Panel> </Tabs.Panels> </Tabs> ); }
Tip: Compound components provide a flexible and declarative API. Libraries like Radix UI and Headless UI use this pattern extensively.

Render Props Pattern

Share code between components using a prop whose value is a function:

import { useState, useEffect } from 'react'; // Component that provides mouse position 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); } // Usage with different render functions function App() { return ( <div> <h1>Mouse Tracker Examples</h1> {/* Display as text */} <MouseTracker render={(position) => ( <p> Mouse position: {position.x}, {position.y} </p> )} /> {/* Display as follower element */} <MouseTracker render={(position) => ( <div style={{ position: 'fixed', left: position.x, top: position.y, width: 20, height: 20, borderRadius: '50%', background: 'blue', pointerEvents: 'none', transform: 'translate(-50%, -50%)', }} /> )} /> </div> ); } // Alternative: Using children as a function function DataFetcher({ url, children }) { 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 children({ data, loading, error }); } // Usage function UserProfile() { return ( <DataFetcher url="/api/user"> {({ data, loading, error }) => { if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>Welcome, {data.name}!</div>; }} </DataFetcher> ); }
Warning: Render props can lead to "callback hell" with nested components. Custom hooks are often a cleaner alternative for sharing stateful logic.

Higher-Order Components (HOC)

A function that takes a component and returns a new component with additional props or behavior:

import { useEffect, useState } from 'react'; // HOC that adds authentication check function withAuth(Component) { return function AuthenticatedComponent(props) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); useEffect(() => { // Check authentication checkAuth().then((authenticated) => { setIsAuthenticated(authenticated); setLoading(false); }); }, []); if (loading) return <div>Loading...</div>; if (!isAuthenticated) return <div>Please log in</div>; return <Component {...props} />; }; } // HOC that adds loading state function withLoading(Component) { return function LoadingComponent({ isLoading, ...props }) { if (isLoading) { return <div className="spinner">Loading...</div>; } return <Component {...props} />; }; } // HOC that adds data fetching function withData(Component, url) { return function DataComponent(props) { 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); }); }, []); return <Component {...props} data={data} loading={loading} error={error} />; }; } // Original component function Dashboard({ data }) { return ( <div> <h1>Dashboard</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); } // Apply multiple HOCs const EnhancedDashboard = withAuth(withData(Dashboard, '/api/dashboard')); // Usage function App() { return <EnhancedDashboard />; }

Custom Hooks Pattern

Extract and reuse stateful logic using custom hooks:

import { useState, useEffect, useCallback } from 'react'; // Custom hook for local storage function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); const setValue = useCallback( (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }, [key, storedValue] ); return [storedValue, setValue]; } // Custom hook for toggle function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue((v) => !v); }, []); return [value, toggle]; } // Custom hook for debounce function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } // Custom hook for window size function useWindowSize() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return windowSize; } // Usage function App() { const [theme, setTheme] = useLocalStorage('theme', 'light'); const [isOpen, toggleOpen] = useToggle(false); const [searchTerm, setSearchTerm] = useState(''); const debouncedSearch = useDebounce(searchTerm, 500); const { width, height } = useWindowSize(); useEffect(() => { // Search when debounced value changes if (debouncedSearch) { console.log('Searching for:', debouncedSearch); } }, [debouncedSearch]); return ( <div className={theme}> <h1>Window Size: {width} x {height}</h1> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> <button onClick={toggleOpen}> {isOpen ? 'Close' : 'Open'} Menu </button> <input type="text" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search..." /> </div> ); }
Tip: Custom hooks are the most common way to share logic in modern React. They're more flexible than HOCs and cleaner than render props.

Provider Pattern

Use React Context to provide global state without prop drilling:

import { createContext, useContext, useState } from 'react'; // Create theme context const ThemeContext = createContext(); // Theme provider component function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } // Custom hook to use theme function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within ThemeProvider'); } return context; } // Components that use theme function Header() { const { theme, toggleTheme } = useTheme(); return ( <header className={theme}> <h1>My App</h1> <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} mode </button> </header> ); } function Content() { const { theme } = useTheme(); return ( <main className={theme}> <p>Current theme: {theme}</p> </main> ); } // App with provider function App() { return ( <ThemeProvider> <Header /> <Content /> </ThemeProvider> ); }

Composition vs Inheritance

React recommends composition over inheritance for code reuse:

// Bad - Using inheritance (not recommended in React) class BaseButton extends React.Component { render() { return <button className="btn">{this.props.children}</button>; } } class PrimaryButton extends BaseButton { render() { return ( <button className="btn btn-primary">{this.props.children}</button> ); } } // Good - Using composition function Button({ variant = 'default', children, ...props }) { const className = `btn btn-${variant}`; return ( <button className={className} {...props}> {children} </button> ); } // Usage function App() { return ( <div> <Button variant="default">Default</Button> <Button variant="primary">Primary</Button> <Button variant="danger">Danger</Button> </div> ); } // Composition with slots function Card({ header, footer, children }) { return ( <div className="card"> {header && <div className="card-header">{header}</div>} <div className="card-body">{children}</div> {footer && <div className="card-footer">{footer}</div>} </div> ); } // Usage function Profile() { return ( <Card header={<h2>User Profile</h2>} footer={<button>Edit Profile</button>} > <p>Name: John Doe</p> <p>Email: john@example.com</p> </Card> ); }
Note: Composition is more flexible than inheritance. You can combine simple components to create complex UIs without building deep class hierarchies.

Controlled vs Uncontrolled Components

Understand when to control component state:

import { useState, useRef } from 'react'; // Controlled component - React controls the value function ControlledForm() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const handleSubmit = (e) => { e.preventDefault(); console.log({ name, email }); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" /> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <button type="submit">Submit</button> </form> ); } // Uncontrolled component - DOM controls the value function UncontrolledForm() { const nameRef = useRef(); const emailRef = useRef(); const handleSubmit = (e) => { e.preventDefault(); console.log({ name: nameRef.current.value, email: emailRef.current.value, }); }; return ( <form onSubmit={handleSubmit}> <input ref={nameRef} type="text" placeholder="Name" /> <input ref={emailRef} type="email" placeholder="Email" /> <button type="submit">Submit</button> </form> ); }
Exercise 1: Create a compound component for a dropdown menu with Dropdown, Dropdown.Trigger, Dropdown.Menu, and Dropdown.Item sub-components. Use Context to share state between components.
Exercise 2: Build three custom hooks: useAsync for handling async operations, useForm for form state management, and usePagination for pagination logic. Use them together in a component that fetches and displays paginated data.
Exercise 3: Implement a feature flag system using the Provider pattern. Create a FeatureFlagProvider that loads flags from an API and a useFeature hook that checks if a feature is enabled. Show/hide features based on flags.