If you’ve ever worked on a large React application, you’ve probably encountered messy, hard-to-maintain code. 😵💫
The solution? Design patterns—proven methods that help us write cleaner, reusable, and scalable code. In this post, we’ll dive into the most essential design patterns in React, with practical examples so you can implement them in your projects.
One of the most classic React patterns is Container/Presenter, which separates logic from presentation.
🔥 When to use it? • When a component has too much logic and re-renders frequently. • To separate the data layer from the UI layer for better reusability.
❌ Before (Mixed Logic and UI 😖)
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = React.useState<{ name: string; email: string } | null>(null);
React.useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
if (!user) return <p>Loading...</p>;
return (
<h1>
{user.name} ({user.email})
</h1>
);
}
Here, the component handles both data fetching and UI, making it harder to reuse.
✅ After (Applying Container/Presenter 🤩)
function UserProfilePresenter({ user }: { user: { name: string; email: string } }) {
return <h1>{user.name} ({user.email})</h1>;
}
Container Component (Handles Logic):
function UserProfileContainer({ userId }: { userId: number }) {
const [user, setUser] = React.useState<{ name: string; email: string } | null>(null);
React.useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
if (!user) return <p>Loading...</p>;
return <UserProfilePresenter user={user} />;
}
Now, UserProfilePresenter only handles UI, while UserProfileContainer manages logic.
🔥 When to use it? • When you want to share logic across components without using HOCs. • When child components need control over what gets rendered.
❌ Before (Duplicated Logic in Multiple Components 🤯)
function MouseTracker() {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
return (
<p>
Mouse Position: {position.x}, {position.y}
</p>
);
}
The logic here is tied to the component, making it less reusable.
✅ After (Using Render Props 💡)
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => JSX.Element }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
return render(position);
}
// Usage
function App() {
return (
<MouseTracker
render={(position) => (
<h1>
Mouse is at {position.x}, {position.y}
</h1>
)}
/>
);
}
Now, MouseTracker doesn’t decide how to render the position, leaving control to the parent component.
Custom Hooks are a modern React feature that allows reusing state logic without needing HOCs or Render Props.
🔥 When to use it? • When useState and useEffect are repeated in multiple components. • To separate business logic from UI components.
❌ Before (Duplicated Logic in Multiple Components 🤦♂️)
function UserList() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch("https://api.example.com/users")
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
If another component needs user data, you’ll repeat the same logic.
✅ After (With a Custom Hook 😍)
function useFetch<T>(url: string): T | null {
const [data, setData] = React.useState<T | null>(null);
React.useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data));
}, [url]);
return data;
}
// Using the Hook in Multiple Components
function UserList() {
const users = useFetch<{ id: number; name: string }[]>("https://api.example.com/users");
if (!users) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function ProductList() {
const products = useFetch<{ id: number; name: string }[]>("https://api.example.com/products");
if (!products) return <p>Loading...</p>;
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
Now, useFetch encapsulates the API fetching logic, making it reusable across multiple components.
These design patterns will help you write cleaner, more reusable, and scalable code in React. 🚀