React Design Patterns with Real Examples
As a professional React developer, understanding and utilizing design patterns is essential for building scalable, maintainable, and efficient applications. Here, we will explore some of the most common React patterns and how they can enhance our development workflow.
1. Presentational and Container Components
When to use: Separate logic from UI to make components clean and testable.
Example:
// Container Component
function UserContainer() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser);
}, []);
return <UserProfile user={user} />;
}
// Presentational Component
function UserProfile({ user }) {
if (!user) return <p>Loading...</p>;
return <h1>Welcome, {user.name}!</h1>;
}2. Compound Components
When to use: Build flexible, declarative APIs for UI components like tabs or accordions.
Example: Tabs
const TabsContext = createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
<button
className={index === activeIndex ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
return <div>{children[activeIndex]}</div>;
}
function TabPanel({ children }) {
return <div>{children}</div>;
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;Usage:
<Tabs>
<Tabs.List>
<Tabs.Tab index={0}>Tab 1</Tabs.Tab>
<Tabs.Tab index={1}>Tab 2</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Content for Tab 1</Tabs.Panel>
<Tabs.Panel>Content for Tab 2</Tabs.Panel>
</Tabs.Panels>
</Tabs>3. Render Props
When to use: Share code between components using a function as a child.
Example:
import React, { useState, useEffect } from 'react';
// DataFetcher component that uses render props
const DataFetcher = ({ url, render }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return render({ data, loading, error });
};
// Parent component that uses DataFetcher
const App = () => {
return (
<DataFetcher
url="https://api.example.com/data"
render={({ data, loading, error }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>Fetched Data:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}}
/>
);
};
export default App;4. Higher-Order Components (HOC)
When to use: Add behavior to components without modifying them.
Example:
function withLogger(WrappedComponent) {
return function(props) {
console.log('Rendering', WrappedComponent.name);
return <WrappedComponent {...props} />;
};
}
const Hello = () => <h1>Hello!</h1>;
const HelloWithLogger = withLogger(Hello);5. Custom Hooks
When to use: Extract logic from components to make them more reusable.
Example:
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return data;
}
// Usage
const posts = useFetch('/api/posts');Conclusion
Incorporating these patterns into our React development can significantly improve the structure and quality of our applications. As we grow more familiar with these patterns, we’ll find that they can simplify our code, enhance reusability, and ultimately lead to a better development experience.
Happy coding!
