React.js Fundamentals
React Design Patterns
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.