React Component Design Patterns

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:

  1. Presentational and Container Components
  2. Higher-Order Components (HOCs)
  3. Render Props
  4. Custom Hooks
  5. 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.

React Component Design Patterns

    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.

    Design Patterns in React Component

    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.

    Design Patterns React Component

    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:

    Leave a Comment

    51 Python Programs

    51 PYTHON PROGRAMS PDF FREE

    Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

    pyython developer roadmap

    Aspiring to be a Python developer?

    Download a FREE PDF on how to become a Python developer.

    Let’s be friends

    Be the first to know about sales and special discounts.