Have you ever looked at a React codebase and felt overwhelmed by its complexity, deeply nested components, and repeated logic everywhere? If so, you’re not alone. As an app grows, keeping code organized, readable, and maintainable becomes a real challenge.
This is where React Design Patterns shine. 🌟
Like blueprints for a house, React Design Patterns are time-tested solutions to common problems in React app development. They’re not a specific library or framework, but methods and mindsets that help you structure components and manage state effectively.
In this article, we’ll explore the most important and popular design patterns—from classic to modern—to help you take your React skills to the next level.
Why Care About Design Patterns?
Using design patterns brings many benefits:
- Reusability: Write logic once and reuse it in many places, reducing code duplication (Don’t Repeat Yourself - DRY).
- Maintainability: Well-organized, clear code makes it easier for you and your team to find bugs, fix, and upgrade.
- Scalability: A solid structure makes it easy to add new features without breaking what already works.
- Separation of Concerns: Keep UI logic and business logic separate, making components simpler and more focused.
1. Container and Presentational Components Pattern
This is one of the foundational and classic patterns, clearly separating "data" from "presentation".
-
Presentational Components:
- Role: Only care about how things look. They receive data and handlers via
props
and render the UI. - Traits: Usually stateless functional components, no complex logic, no API calls. Very easy to reuse.
- Example: A
UserList
component that just receives ausers
array and displays it.
- Role: Only care about how things look. They receive data and handlers via
-
Container Components:
- Role: Care about how things work. They contain business logic, manage state, and fetch data from APIs.
- Traits: They provide data and handlers (callbacks) to Presentational Components via
props
. - Example: A
UserListContainer
component fetches the user list, stores it in state, and passes it toUserList
.
Practical Example:
// 1. Presentational Component (just for display)
const UserList = ({ users }) => {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
// 2. Container Component (handles logic, data fetching)
const UserListContainer = () => {
const [users, setUsers] = useState([])
useEffect(() => {
// Simulate API call
fetch('https://api.example.com/users')
.then((res) => res.json())
.then((data) => setUsers(data))
}, [])
return <UserList users={users} />
}
✨ Benefit: This pattern makes it easy to change the UI without affecting logic, and vice versa. It also encourages reusing presentational components.
2. Higher-Order Components (HOCs)
A HOC is an advanced pattern for reusing component logic. Basically, a HOC is a function that takes a component and returns a new component with enhanced props.
Think of a HOC as a decorator for your component. You give it a plain component, and it "decorates" it with extra features (props) like data, handlers, etc., then returns it to you.
Example: Creating a HOC for Logging
// This is a HOC: a function that takes a Component
const withLogger = (WrappedComponent) => {
// and returns a new component
return (props) => {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} mounted.`)
}, [])
// Render the original component with its props
return <WrappedComponent {...props} />
}
}
// Original component
const MyComponent = ({ message }) => <div>{message}</div>
// Use the HOC to wrap MyComponent
const ComponentWithLogger = withLogger(MyComponent)
// When you render <ComponentWithLogger message="Hello HOC!" />,
// the message will be logged to the console.
🔑 When to use: HOCs are great for cross-cutting concerns like authentication, logging, connecting to Redux store (connect
from react-redux
is a famous HOC).
3. Render Props Pattern
Like HOCs, Render Props is a technique for sharing code between components. Instead of wrapping, it uses a prop
that is a function to share logic.
The component calls this function and passes in the data it manages. The function returns JSX to render. The prop doesn’t have to be called render
—it’s often children
for cleaner syntax.
Example: Mouse Position Tracker Component
const MouseTracker = ({ children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 })
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY,
})
}
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{/* Call the children function and pass state.
This function decides what to render.
*/}
{children(position)}
</div>
)
}
// Usage
const App = () => {
return (
<MouseTracker>
{/* This is the function passed to `children` */}
{(mousePosition) => (
<h1>
Mouse position: ({mousePosition.x}, {mousePosition.y})
</h1>
)}
</MouseTracker>
)
}
✨ Compared to HOC: Render Props are often clearer and more flexible than HOCs because you can see where props come from right in the render function. It also avoids "wrapper hell" (too many nested components) that HOCs can cause.
4. Provider Pattern (with React Context)
Have you ever had to pass props through many component layers just so a deep child can use them? This is called prop drilling. The Provider Pattern, implemented by the React Context API, is the perfect solution.
This pattern lets you create a global data source (provider) that any child component can access directly, without passing props through every level.
Example: Creating a Theme Provider for your app
// 1. Create Context
const ThemeContext = React.createContext('light') // Default value
// 2. Create Provider Component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
}
return (
// Provide 'theme' and 'toggleTheme' to the whole subtree
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Use in App
const App = () => {
return (
<ThemeProvider>
<Toolbar />
</ThemeProvider>
)
}
// 4. Child component uses Context
const Toolbar = () => {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)
}
🚀 When to use: Ideal for sharing global data like user info, theme (light/dark), language, etc.
5. Custom Hooks Pattern
This is the most modern and powerful pattern in React today. With the advent of Hooks (useEffect, useState), we have a great way to extract and reuse stateful logic from components.
A Custom Hook is basically a JavaScript function starting with use
that can call other Hooks inside it.
Example: Creating a useFetch
custom hook for API calls
Instead of repeating useState
and useEffect
logic in every component that fetches data, we can create a hook.
// Custom Hook
const useFetch = (url) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => setError(err))
.finally(() => setLoading(false))
}, [url]) // Runs again when url changes
return { data, loading, error }
}
// Usage in a component
const UserProfile = ({ userId }) => {
const {
data: user,
loading,
error,
} = useFetch(`https://api.example.com/users/${userId}`)
if (loading) return <p>Loading...</p>
if (error) return <p>Something went wrong!</p>
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
)
}
🏆 Why are Custom Hooks the top choice? Custom Hooks solve the problem of sharing logic in the cleanest way. They don’t add extra component nesting like HOCs or Render Props, keeping the component tree flat and code very readable. This is the community-recommended approach in modern React.
Conclusion: Which Pattern Should You Use?
There’s no single "best" pattern for every case. The choice depends on the problem you’re solving:
- Separate UI and logic? Use Container/Presentational.
- Share global data? Use the Provider Pattern (Context API).
- Reuse stateful logic? Custom Hooks are the top choice in modern React. HOCs and Render Props are still useful, especially with legacy codebases or class components.
Mastering these design patterns not only helps you write better React code, but also trains you to solve problems systematically. Start applying them to your projects today, and you’ll see a clear difference in code quality and maintainability.
Good luck on your React journey!