While working on a large React project for a U.S.-based e-commerce client, I ran into a familiar problem: my components were getting messy. They were doing too much, and reusing them across different pages became difficult.
That’s when I revisited some of my favorite React component design patterns, the same ones I’ve refined over the last 8+ years of building production-grade React applications.
In this tutorial, I’ll share the patterns I use most often, why they matter, and how you can apply them in your projects.
Use React Design Patterns
React gives you flexibility, but that flexibility can lead to chaos if you don’t follow consistent patterns.
Design patterns help you:
- Make your code easier to read and maintain.
- Reuse logic across multiple components.
- Improve performance and scalability.
In this guide, we’ll cover practical patterns like:
- Presentational and Container Components
- Higher-Order Components (HOCs)
- Render Props
- Custom Hooks
- Compound Components
Each pattern includes a real-world example with full code.
Method 1 – Presentational and Container Components
This is one of the simplest and most effective patterns. I use it to separate UI logic from data-fetching or state logic.
Example: Display Product Data
Let’s say we’re building a product listing page for a U.S. retailer.
// ProductListContainer.js
import React, { useEffect, useState } from "react";
import ProductList from "./ProductList";
const ProductListContainer = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("https://fakestoreapi.com/products")
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return <ProductList products={products} />;
};
export default ProductListContainer;// ProductList.js
import React from "react";
const ProductList = ({ products }) => {
return (
<div>
<h2>Available Products</h2>
<ul>
{products.map(p => (
<li key={p.id}>{p.title} - ${p.price}</li>
))}
</ul>
</div>
);
};
export default ProductList;I executed the above example code and added the screenshot below.

Here,
- ProductListContainer handles fetching data.
- ProductList only focuses on how the data looks.
This separation makes your code cleaner and reusable.
Method 2 – Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new component. I often use them for shared logic, like authentication or logging.
Example: Add Logging to Components
// withLogger.js
import React, { useEffect } from "react";
const withLogger = (WrappedComponent) => {
return (props) => {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} mounted`);
}, []);
return <WrappedComponent {...props} />;
};
};
export default withLogger;// Dashboard.js
import React from "react";
import withLogger from "./withLogger";
const Dashboard = () => {
return <h2>Welcome to the Dashboard</h2>;
};
export default withLogger(Dashboard);I executed the above example code and added the screenshot below.

Now, every time the Dashboard mounts, it logs a message. This pattern keeps your code DRY (Don’t Repeat Yourself).
Method 3 – Render Props
Render Props let you share logic between components using a function prop. I use this when multiple components need the same behavior but different UIs.
Example: Mouse Tracker
// MouseTracker.js
import React, { useState } from "react";
const MouseTracker = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: "200px", border: "1px solid gray" }}>
{render(position)}
</div>
);
};
export default MouseTracker;// App.js
import React from "react";
import MouseTracker from "./MouseTracker";
const App = () => {
return (
<MouseTracker
render={({ x, y }) => (
<p>Mouse Position: ({x}, {y})</p>
)}
/>
);
};
export default App;I executed the above example code and added the screenshot below.

The render prop gives you flexibility; you can change the UI while keeping the logic the same.
Method 4 – Custom Hooks
Since React introduced Hooks, this has become my favorite way to share logic. Custom Hooks are simple, reusable, and elegant.
Example: Fetch Data with a Custom Hook
// useFetch.js
import { useState, useEffect } from "react";
const useFetch = (url) => {
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 { data, loading };
};
export default useFetch;// Users.js
import React from "react";
import useFetch from "./useFetch";
const Users = () => {
const { data: users, loading } = useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
};
export default Users;With this approach, you can use useFetch anywhere in your app, clean and reusable.
Method 5 – Compound Components
Compound Components are great when you want flexibility in how users compose your UI.
I often use this for modals, dropdowns, or tabs.
Example: Custom Modal Component
// Modal.js
import React, { useState, createContext, useContext } from "react";
const ModalContext = createContext();
export const Modal = ({ children }) => {
const [open, setOpen] = useState(false);
return (
<ModalContext.Provider value={{ open, setOpen }}>
{children}
</ModalContext.Provider>
);
};
Modal.OpenButton = ({ children }) => {
const { setOpen } = useContext(ModalContext);
return <button onClick={() => setOpen(true)}>{children}</button>;
};
Modal.Content = ({ children }) => {
const { open, setOpen } = useContext(ModalContext);
if (!open) return null;
return (
<div className="modal">
<div className="modal-content">
{children}
<button onClick={() => setOpen(false)}>Close</button>
</div>
</div>
);
};// App.js
import React from "react";
import { Modal } from "./Modal";
const App = () => {
return (
<Modal>
<Modal.OpenButton>Open Modal</Modal.OpenButton>
<Modal.Content>
<h3>Welcome!</h3>
<p>This is a reusable modal component.</p>
</Modal.Content>
</Modal>
);
};
export default App;This pattern gives developers more control over how to structure the component while keeping the logic encapsulated.
Best Practices I Follow
Over the years, I’ve learned a few best practices that make these patterns even more effective:
- Keep components small and focused.
- Prefer composition over inheritance.
- Use TypeScript for better type safety.
- Always write unit tests for reusable logic.
- Document your patterns for your team.
These small habits make a big difference in long-term maintainability.
When I started following these patterns consistently, my React codebase became cleaner, easier to debug, and far more scalable. If you’re working on a large project, especially one that will grow over time, adopting these design patterns will save you countless hours.
You may also like to read:
- Build a Reusable Icon Component in React
- Add a CSS Class to a React Component
- How to Build an Accordion Component in React
- Build a Simple React Tree View Component

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.