DEV Community

Cover image for React Mastery Series – Day 28: React Design Patterns – Compound Components, Render Props, Higher-Order Components & Custom Hooks
Siva Samanthapudi
Siva Samanthapudi

Posted on

React Mastery Series – Day 28: React Design Patterns – Compound Components, Render Props, Higher-Order Components & Custom Hooks

Welcome back to the React Mastery Series!

In the previous article, we explored Authentication & Authorization and learned how enterprise React applications implement:

  • JWT Authentication
  • Protected Routes
  • Role-Based Access Control (RBAC)
  • Axios Interceptors
  • Token Refresh
  • Enterprise authentication architecture

Today, we'll move beyond React APIs and explore something that separates intermediate developers from senior engineers:

React Design Patterns

As applications grow, writing React code that simply works isn't enough.

We need code that is:

  • Reusable
  • Maintainable
  • Scalable
  • Easy to test
  • Easy to extend

This is where design patterns come into play.


What are Design Patterns?

A design pattern is a proven solution to a commonly occurring software design problem.

Instead of reinventing solutions every time, developers use patterns that have already proven effective in large applications.

Think of them as reusable blueprints for structuring your code.


Why Design Patterns Matter

Imagine you're building an e-commerce application.

You'll have:

  • Product Cards
  • Shopping Cart
  • Checkout
  • User Profile
  • Orders
  • Wishlist

Without patterns:

  Large Components
        ↓
  Duplicate Logic
        ↓
Difficult Maintenance
        ↓
   Frequent Bugs
Enter fullscreen mode Exit fullscreen mode

With patterns:

Reusable Components
        ↓
   Shared Logic
        ↓
   Cleaner Code
        ↓
 Easy Maintenance
Enter fullscreen mode Exit fullscreen mode

Pattern 1 – Composition

React encourages Composition over Inheritance.

Instead of creating deeply nested class hierarchies, React allows components to be composed together.

Example:

type CardProps = {
  children: React.ReactNode;
};

function Card({ children }: CardProps) {
  return <div className="card">{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Usage:

<Card>
  <h2>Bank Account</h2>

  <p>Balance: $10,000</p>
</Card>
Enter fullscreen mode Exit fullscreen mode

The Card component doesn't know what content it displays.

It simply provides a reusable layout.


Pattern 2 – Compound Components

Compound Components work together as a single unit.

Examples:

  • Accordion
  • Tabs
  • Modal
  • Select
  • Menu

Instead of:

<Accordion
  title="React"
  content="React is a library."
/>
Enter fullscreen mode Exit fullscreen mode

We compose smaller pieces.

<Accordion>
  <Accordion.Header>
    React
  </Accordion.Header>

  <Accordion.Body>
    React is a library.
  </Accordion.Body>
</Accordion>
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Flexible API
  • Cleaner JSX
  • Better readability

Real-World Example

Imagine building a banking dashboard card.

<Card>
  <Card.Header>
    Savings Account
  </Card.Header>

  <Card.Body>
    AED 52,000
  </Card.Body>

  <Card.Footer>
    Updated 2 mins ago
  </Card.Footer>
</Card>
Enter fullscreen mode Exit fullscreen mode

Each section has a clear responsibility.


Pattern 3 – Custom Hooks

Earlier in this series, we learned how to create Custom Hooks.

They are also a powerful design pattern.

Without Custom Hooks:

    Dashboard
        
Authentication Logic
        
    API Logic
        
  Loading Logic
Enter fullscreen mode Exit fullscreen mode

Every page repeats the same code.


With Custom Hooks:

     Dashboard
        
     useAuth()
        
   useAccounts()
        
useNotifications()
Enter fullscreen mode Exit fullscreen mode

Business logic becomes reusable.

Example:

function useUser() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Fetch user
  }, []);

  return user;
}
Enter fullscreen mode Exit fullscreen mode

Now multiple components can reuse the same logic.


Pattern 4 – Render Props

A Render Prop is a function passed as a prop that returns JSX.

Example:

type DataLoaderProps = {
  render: (data: string[]) => React.ReactNode;
};

function DataLoader({
  render,
}: DataLoaderProps) {
  const users = ["Alice", "Bob"];

  return <>{render(users)}</>;
}
Enter fullscreen mode Exit fullscreen mode

Usage:

<DataLoader
  render={(users) => (
    <ul>
      {users.map((user) => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

The component handles data fetching, while the parent decides how to display it.

Although Custom Hooks have replaced many Render Prop use cases, you'll still encounter this pattern in existing codebases.


Pattern 5 – Higher-Order Components (HOC)

A Higher-Order Component is a function that takes a component and returns a new component with additional behavior.

     Component
        ↓
       HOC
        ↓
Enhanced Component
Enter fullscreen mode Exit fullscreen mode

Example:

function withLoading<T>(Component: React.ComponentType<T>) {
  return function WrappedComponent(props: T & { loading: boolean }) {
    if (props.loading) {
      return <p>Loading...</p>;
    }

    return <Component {...props} />;
  };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const UserListWithLoading = withLoading(UserList);
Enter fullscreen mode Exit fullscreen mode

Common HOC use cases:

  • Authentication
  • Authorization
  • Logging
  • Analytics
  • Error handling

Modern React often prefers Custom Hooks, but HOCs remain important in many enterprise applications.


Pattern 6 – Provider Pattern

The Provider Pattern shares data across the application.

Example:

<AuthProvider>
  <ThemeProvider>
    <App />
  </ThemeProvider>
</AuthProvider>
Enter fullscreen mode Exit fullscreen mode

Instead of passing props through many components:

  App
   ↓
Dashboard
   ↓
 Sidebar
   ↓
 Profile
   ↓
 Avatar
Enter fullscreen mode Exit fullscreen mode

Any component can access shared data through Context.


Pattern 7 – Controlled Component Pattern

Many reusable components support both controlled and uncontrolled modes.

Example:

type ToggleProps = {
  checked: boolean;
  onChange: () => void;
};

function Toggle({checked, onChange,}: ToggleProps) {
  return (
    <button onClick={onChange}>
      {checked ? "ON" : "OFF"}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

The parent controls the component's state.

This pattern is widely used in UI libraries like Material UI and Ant Design.


Pattern 8 – Container & Presentational Components

Separate business logic from presentation.

Container:

function UserContainer() {
  const users = useUsers();

  return <UserList users={users} />;
}
Enter fullscreen mode Exit fullscreen mode

Presentational Component:

type UserListProps = {
  users: string[];
};

function UserList({users}: UserListProps) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Responsibilities:

Container Presentational
API Calls UI Rendering
Business Logic Layout
State Management Display Data

Pattern 9 – Feature-Based Architecture

Enterprise React applications organize code by features.

src

├── features
│   ├── auth
│   ├── dashboard
│   ├── accounts
│   ├── payments
│   └── transactions
Enter fullscreen mode Exit fullscreen mode

Each feature contains:

accounts

├── components
├── hooks
├── pages
├── services
├── types
Enter fullscreen mode Exit fullscreen mode

This keeps related files together and improves team collaboration.


Enterprise Example

Imagine an online banking application.

       Dashboard
          ↓
    Account Summary
          ↓
  Transaction List
          ↓
     Loan Details
          ↓
Investment Portfolio
Enter fullscreen mode Exit fullscreen mode

Each module uses:

  • Custom Hooks
  • Shared Components
  • Context Providers
  • Feature Modules
  • API Services

This architecture scales well even as the application grows.


Choosing the Right Pattern

Problem Recommended Pattern
Shared UI Composition
Shared Logic Custom Hooks
Global State Provider Pattern
Flexible Components Compound Components
Enhance Components HOCs
Separate UI & Logic Container/Presentational
Large Applications Feature-Based Architecture

Common Mistakes

1. Using Patterns Too Early

Not every component needs an advanced design pattern.

Start simple.

Refactor when duplication or complexity appears.


2. Creating Massive Components

Avoid components with:

  • API calls
  • Business logic
  • Forms
  • Tables
  • Charts
  • Navigation

all inside one file.

Break them into smaller, focused components.


3. Duplicating Business Logic

If the same logic appears in multiple places, consider moving it into:

  • A Custom Hook
  • A Service
  • A Utility Function

4. Choosing Inheritance Over Composition

React is designed around composition.

Prefer combining small components rather than extending large ones.


Best Practices

  • Prefer composition over inheritance.
  • Keep components focused on a single responsibility.
  • Reuse business logic through Custom Hooks.
  • Organize code by feature for large applications.
  • Use Compound Components for flexible APIs.
  • Separate business logic from presentation.
  • Introduce patterns only when they solve a real problem.

Key Takeaways

Today, we learned:

✅ Design patterns improve scalability and maintainability.
✅ Composition is the foundation of React architecture.
✅ Compound Components create expressive and flexible APIs.
✅ Custom Hooks are the preferred way to share business logic.
✅ Higher-Order Components are still relevant in many enterprise projects.
✅ Provider Pattern simplifies global state sharing.
✅ Feature-based architecture helps large teams organize code effectively.


Coming Next 🚀

In Day 29, we will explore:

React Error Handling – Error Boundaries, Logging & Resilient UI Design

We will learn:

  • What happens when React crashes
  • Error Boundaries
  • Handling rendering errors
  • Global error logging
  • Async error handling
  • Fallback UI
  • Monitoring with tools like Sentry
  • Building fault-tolerant React applications

By the end of the next article, you'll know how enterprise applications gracefully recover from unexpected errors instead of showing users a blank screen.

Happy Coding! 🚀

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

I particularly appreciated the section on Compound Components, as it highlights the flexibility and readability benefits of breaking down complex components into smaller, composable pieces. The example of the Accordion component demonstrates how this pattern can lead to a more flexible API and cleaner JSX. I've found that using Compound Components in my own projects has made it easier to reuse and maintain code, especially when working with complex interfaces. Have you found any challenges or trade-offs when implementing Compound Components in larger-scale applications, such as managing state or handling nested component updates?