Why Use Component Design Patterns?

As your application grows, you'll find yourself needing to create components that are not just reusable, but also flexible and intuitive to use. Design patterns provide proven solutions to common problems in component architecture. Let's explore two classic patterns.

Pattern 1: Compound Components

The Compound Component pattern allows you to create components that work together to manage a shared state and logic, providing a more expressive and declarative API for the consumer.

Think about native HTML elements like <select> and <option>. Neither is very useful on its own, but together they create a complete dropdown experience. The <select> element implicitly manages the state (which option is selected) and shares it with its <option> children. We can replicate this pattern in React.

How it Works: We use React.Context to create an implicit connection between a parent provider component and its children.

Example: A Custom Tabs Component Let's build a <Tabs> component system that lets a user write markup like this:

JavaScript


<Tabs>
  <Tabs.List>
    <Tabs.Tab>Tab 1</Tabs.Tab>
    <Tabs.Tab>Tab 2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Content for tab 1</Tabs.Panel>
    <Tabs.Panel>Content for tab 2</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

Implementation (Tabs.js):

JavaScript


import { createContext, useContext, useState } from 'react';

// 1. Create a context to hold the shared state
const TabsContext = createContext();

// 2. The main provider component that manages the state
function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      {children}
    </TabsContext.Provider>
  );
}

// 3. Child components that consume the context
function Tab({ children, index }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  const isActive = index === activeIndex;
  return (
    <button
      className={isActive ? 'active' : ''}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
}

function Panel({ children, index }) {
  const { activeIndex } = useContext(TabsContext);
  return index === activeIndex ? <div>{children}</div> : null;
}

// 4. Attach the children as properties of the main component
Tabs.Tab = Tab;
Tabs.Panel = Panel;
// You can add more like Tabs.List, Tabs.Panels for better structure
// These are simple wrappers that just render children.

export default Tabs;

This pattern creates a clean, self-contained system where the state management is hidden from the user, who can simply focus on composing the UI.

Pattern 2: Higher-Order Components (HOCs)

A Higher-Order Component (HOC) is a function that takes a component as an argument and returns a new, enhanced component. It's a pattern for reusing component logic.

What they're good for:

  • Injecting props or state.
  • Adding wrappers or styling.
  • Handling cross-cutting concerns like logging, data fetching, or authentication.

Example: A withLogger HOC Let's create an HOC that logs to the console whenever the component it wraps mounts.

JavaScript


import { useEffect } from 'react';

// This is the HOC. It's a function that takes a component...
function withLogger(WrappedComponent) {
  // ...and returns a new component.
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log(`Component ${WrappedComponent.name} has mounted.`);
    }, []);

    // Render the original component with its props
    return <WrappedComponent {...props} />;
  };
}

// --- How to use it ---

// A simple component we want to add logging to
function MyUserProfile({ name }) {
  return <h1>Hello, {name}</h1>;
}

// Create the enhanced component by wrapping it with our HOC
const UserProfileWithLogger = withLogger(MyUserProfile);

// Now, when you use <UserProfileWithLogger name="Alice" />,
// it will render the user profile AND log a message to the console.
export default UserProfileWithLogger;

The Rise of Hooks: While HOCs are a powerful pattern, the introduction of React Hooks has provided a more straightforward and composable way to share non-visual logic. For many use cases (like data fetching or logging), a custom hook is now the preferred approach because it avoids "wrapper hell" (deeply nested components in the React DevTools). However, HOCs are still useful, especially for logic that needs to manipulate the component itself, like adding a style wrapper.