Design Patterns in React
Learn proven patterns for building flexible, maintainable, and reusable components. These patterns solve common problems in React development.
🔄 Container/Presentational Pattern
Separate logic (container) from UI (presentational).
// Presentational Component - only UI
function UserListUI({ users, loading, error }) {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Container Component - logic & data fetching
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
return <UserListUI users={users} loading={loading} error={error} />;
}
🎣 Custom Hook Pattern
// Extract logic into custom hook
function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
return { users, loading, error };
}
// Use in component
function UserList() {
const { users, loading, error } = useUsers();
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
🎭 Render Props Pattern
// Component with render prop
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return render({ data, loading });
}
// Usage
function App() {
return (
<div>
<DataFetcher
url="/api/users"
render={({ data, loading }) => (
loading ? <Spinner /> : (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
)}
/>
<DataFetcher
url="/api/posts"
render={({ data, loading }) => (
loading ? <Spinner /> : (
<div>
{data.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
)
)}
/>
</div>
);
}
🏗️ Higher-Order Component (HOC)
// HOC that adds loading behavior
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <Component {...props} />;
};
}
// Original component
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
function App() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
return (
<UserListWithLoading
isLoading={loading}
users={users}
/>
);
}
// Authentication HOC
function withAuth(Component) {
return function WithAuthComponent(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);
🧩 Compound Components
// Context for shared state
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={activeTab === id ? 'active' : ''}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }) {
const { activeTab } = useContext(TabsContext);
return activeTab === id ? <div>{children}</div> : null;
}
// Export as compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage
function App() {
return (
<Tabs defaultTab="home">
<Tabs.List>
<Tabs.Tab id="home">Home</Tabs.Tab>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="home">Home content</Tabs.Panel>
<Tabs.Panel id="profile">Profile content</Tabs.Panel>
<Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>
);
}
🎛️ Control Props Pattern
// Component can be controlled or uncontrolled
function Counter({ value, onChange, defaultValue = 0 }) {
// Uncontrolled state
const [internalValue, setInternalValue] = useState(defaultValue);
// Use controlled value if provided, otherwise use internal
const isControlled = value !== undefined;
const currentValue = isControlled ? value : internalValue;
const handleIncrement = () => {
const newValue = currentValue + 1;
if (!isControlled) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
const handleDecrement = () => {
const newValue = currentValue - 1;
if (!isControlled) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
return (
<div>
<button onClick={handleDecrement}>-</button>
<span>{currentValue}</span>
<button onClick={handleIncrement}>+</button>
</div>
);
}
// Uncontrolled usage
<Counter defaultValue={5} />
// Controlled usage
function App() {
const [count, setCount] = useState(0);
return <Counter value={count} onChange={setCount} />;
}
🎪 Props Getter Pattern
function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const getToggleProps = (props = {}) => ({
...props,
onClick: (e) => {
setIsOpen(!isOpen);
props.onClick?.(e);
},
'aria-expanded': isOpen,
'aria-haspopup': true
});
const getMenuProps = (props = {}) => ({
...props,
hidden: !isOpen,
role: 'menu'
});
const getItemProps = (props = {}) => ({
...props,
role: 'menuitem',
onClick: (e) => {
setIsOpen(false);
props.onClick?.(e);
}
});
return {
isOpen,
getToggleProps,
getMenuProps,
getItemProps
};
}
// Usage
function Dropdown() {
const { getToggleProps, getMenuProps, getItemProps } = useDropdown();
return (
<div>
<button {...getToggleProps()}>
Menu
</button>
<ul {...getMenuProps()}>
<li {...getItemProps({ onClick: () => console.log('Edit') })}>
Edit
</li>
<li {...getItemProps({ onClick: () => console.log('Delete') })}>
Delete
</li>
</ul>
</div>
);
}
🎨 State Reducer Pattern
// Built-in reducer
const defaultReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
function useCounter({ initialCount = 0, reducer = defaultReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { count: initialCount });
const increment = () => dispatch({ type: 'INCREMENT' });
const decrement = () => dispatch({ type: 'DECREMENT' });
return { count: state.count, increment, decrement };
}
// Usage - default behavior
function Counter1() {
const { count, increment, decrement } = useCounter();
return (
<div>
<button onClick={decrement}>-</button>
{count}
<button onClick={increment}>+</button>
</div>
);
}
// Usage - custom reducer with max limit
function Counter2() {
const customReducer = (state, action) => {
const newState = defaultReducer(state, action);
// Enforce max of 10
return { count: Math.min(newState.count, 10) };
};
const { count, increment, decrement } = useCounter({
reducer: customReducer
});
return (
<div>
{count} (max: 10)
<button onClick={increment}>+</button>
</div>
);
}
📊 Pattern Comparison
| Pattern | Use Case | Pros | Cons |
|---|---|---|---|
| Custom Hook | Share stateful logic | Simple, composable | Can't share JSX |
| Render Props | Share rendering logic | Flexible JSX | Callback hell |
| HOC | Enhance components | Reusable wrappers | Prop name collisions |
| Compound | Related components | Flexible API | More complex |
| Control Props | Flexible control | Controlled/uncontrolled | More code |
🎯 Key Takeaways
- Custom Hooks: Most common pattern for sharing logic
- Render Props: When you need to share JSX rendering
- HOC: Add behavior to existing components
- Compound Components: Related components working together
- Control Props: Let users control component state
- Props Getters: Provide correct props automatically
- State Reducer: Allow custom state management logic
- Choose wisely: Pick pattern based on your needs