DEV Community

Cover image for Component Design Patterns in React: Compound Components and Render Props in Practice
DEVANSHU PATIL
DEVANSHU PATIL

Posted on AI-assisted

Component Design Patterns in React: Compound Components and Render Props in Practice

Component Design Patterns in React: Compound Components and Render Props in Practice

title: "Component Design Patterns in React: Compound Components and Render Props in Practice"
published: true
published_at: "2026-11-19T09:00:00+05:30"
description: "Explore advanced React component design patterns. Learn how to implement context-driven compound components like accordions and modals, and utilize render props for flexible, decoupled design systems while avoiding prop drilling."
tags: [react, javascript, architecture, webdev]
ai_disclosure_level: some_ai

Building scalable, maintainable, and flexible design systems in React requires moving beyond basic prop-passing. As applications grow, components often accumulate dozens of optional props, leading to tight coupling, poor readability, and the dreaded prop drilling. Advanced component design patterns like Compound Components and Render Props provide elegant architectural solutions to these challenges.

In this article, we will examine how to leverage React's Context API for compound components and how to use render props to create inversion of control in your UI components.

1. The Problem: The Monolithic Component Anti-Pattern

Consider a standard <Dropdown /> component designed to handle various use cases. Initially, it might look straightforward:

// Anti-Pattern: Prop Explosion
function Dropdown({ trigger, items, onSelect, isOpen, position }) {
  // Implementation details
}
Enter fullscreen mode Exit fullscreen mode

As requirements evolve, developers add props for animations, custom renderers, accessibility attributes, keyboard navigation handlers, and sub-components. The API surface explodes, maintenance becomes painful, and readability plummets. Compound components and render props solve this by inverting control and delegating rendering responsibilities to the consumer.

2. Compound Components with React Context

Compound components are a pattern where two or more components work together to have a shared state. Think of native HTML elements like <select> and <option>: neither is very useful without the other, and they implicitly share state.

By combining React's Context API with compound components, we can build flexible APIs like <Accordion>, <Tabs>, or <Menu> without exposing internal state management to the consumer.

Building a Flexible Accordion

Let's build a compound Accordion component. We want the consumer to control the layout and order of items while the compound parent manages which accordion section is currently expanded.

import React, { createContext, useContext, useState } from "react";

// 1. Create Context for shared state
const AccordionContext = createContext();

export function Accordion({ children, defaultIndex = null }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);

  const toggleIndex = (index) => {
    setActiveIndex((prevIndex) => (prevIndex === index ? null : index));
  };

  return (
    <AccordionContext.Provider value={{ activeIndex, toggleIndex }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

// 2. Sub-component: Item
function AccordionItem({ index, children }) {
  const { activeIndex, toggleIndex } = useContext(AccordionContext);
  const isOpen = activeIndex === index;

  return (
    <div className="accordion-item">
      {React.Children.map(children, (child) =>
        React.cloneElement(child, { isOpen, index, toggleIndex })
      )}
    </div>
  );
}

// 3. Sub-component: Header
function AccordionHeader({ children, index, toggleIndex, isOpen }) {
  return (
    <button
      className={`accordion-header ${isOpen ? "active" : ""}`}
      onClick={() => toggleIndex(index)}
      aria-expanded={isOpen}
    >
      {children}
    </button>
  );
}

// 4. Sub-component: Panel
function AccordionPanel({ children, isOpen }) {
  if (!isOpen) return null;
  return <div className="accordion-panel">{children}</div>;
}

// Attach sub-components to the main component
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;
Enter fullscreen mode Exit fullscreen mode

Consuming the Compound Accordion

Notice how clean the consumption API is. The consumer controls the exact DOM structure and styling classes, while the component handles the underlying accessibility and state logic:

export function App() {
  return (
    <Accordion defaultIndex={0}>
      <Accordion.Item index={0}>
        <Accordion.Header>Section 1</Accordion.Header>
        <Accordion.Panel>Content for section one goes here.</Accordion.Panel>
      </Accordion.Item>
      <Accordion.Item index={1}>
        <Accordion.Header>Section 2</Accordion.Header>
        <Accordion.Panel>Content for section two goes here.</Accordion.Panel>
      </Accordion.Item>
    </Accordion>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Render Props Pattern

The Render Prop pattern refers to a technique for sharing code between React components using a prop whose value is a function. A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.

This pattern provides maximum flexibility for cross-cutting concerns such as data fetching, window resizing, mouse tracking, or form state management.

Implementing a Data Fetching / Mouse Tracker Component

Let's create a reusable component that tracks mouse coordinates and exposes them via a render prop, allowing the consumer to render any UI they want using those coordinates.

import React, { useState } from "react";

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({
      x: event.clientX,
      y: event.clientY,
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Consuming the Render Prop

export function Canvas() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          <h1>Move the mouse around!</h1>
          <p>
            The current cursor position is ({x}, {y})
          </p>
        </div>
      )}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: With the advent of React Hooks, custom hooks have largely replaced render props for state logic sharing. However, render props remain invaluable when rendering flexibility is strictly required at the JSX composition level.

4. Comparing Compound Components vs. Render Props

Feature Compound Components Render Props
Primary Goal Grouping tightly coupled UI elements sharing state. Delegating rendering logic to external callers.
State Access Implicit via React Context (useContext). Explicit via function arguments.
API Readability Highly declarative (<Menu.Item />). Can lead to callback nesting if overused.
Best Used For Accordions, Tabs, Menus, Select dropdowns. Mouse tracking, dynamic lists, view controllers.

5. Summary and Best Practices

  1. Favor Declarative APIs: Design your component interfaces so developers can easily understand the hierarchy at a glance.
  2. Avoid Over-Engineering: Simple components do not need compound architectures. Use these patterns when flexibility and multiple sub-parts are genuinely required.
  3. Combine Patterns: You can combine Context-driven compound components with render props inside individual sub-components to achieve ultimate power and customization in a design system.

By mastering compound components and render props, you can build robust, extensible, and clean React component libraries that scale gracefully across teams.

Top comments (0)