React Composition

Master React patterns like compound components, render props, and higher-order components for clean reusable patterns.

TL;DR

  1. 01Use compound components to create flexible component APIs.
  2. 02Use render props to share logic through children functions.
  3. 03Use higher-order components to wrap and enhance existing components.

Tips

  1. 01Use custom hooks instead of render props or HOCs for new code, since hooks are simpler and easier to understand.

Warnings

  1. 01Deeply nested HOCs create hard-to-debug code — prefer flat composition using multiple custom hooks instead.

Compound Components

  • Build components that work together as a group with shared state.
    const Accordion = ({ children }) => {
      const [active, setActive] = useState(null);
      return (
        <AccordionContext.Provider value={{ active, setActive }}>
          {children}
        </AccordionContext.Provider>
      );
    };
    
    const AccordionItem = ({ id, title, children }) => {
      const { active, setActive } = useContext(AccordionContext);
      return (
        <div>
          <button onClick={() => setActive(id)}>{title}</button>
          {active === id && <div>{children}</div>}
        </div>
      );
    };
    
    <Accordion>
      <AccordionItem id="1" title="Section 1">Content 1</AccordionItem>
      <AccordionItem id="2" title="Section 2">Content 2</AccordionItem>
    </Accordion>
  • Compound components are flexible because the child components control their appearance.
  • They share state through Context instead of passing props through every level.
  • Use this pattern for tightly coupled components like form fields and labels.

Render Props

  • Pass a function as a prop to let child components decide what to render.
    const MouseTracker = ({ render }) => {
      const [pos, setPos] = useState({ x: 0, y: 0 });
      
      const handleMouseMove = (e) => {
        setPos({ x: e.clientX, y: e.clientY });
      };
      
      return (
        <div onMouseMove={handleMouseMove}>
          {render(pos)}
        </div>
      );
    };
    
    <MouseTracker render={({ x, y }) => (
      <p>Mouse at {x}, {y}</p>
    )} />
  • The render function receives data and returns JSX to display.
  • This lets you reuse logic without creating a wrapper component.
  • Render props are flexible because the caller decides what to render.
  • The children function is a special render prop pattern.
    <MouseTracker>
      {({ x, y }) => <p>Position: {x}, {y}</p>}
    </MouseTracker>

Higher-Order Components (HOC)

  • Wrap a component to add or modify its behavior.
    const withAuth = (Component) => {
      return (props) => {
        const [user, setUser] = useState(null);
        const [loading, setLoading] = useState(true);
        
        useEffect(() => {
          checkAuth().then(u => {
            setUser(u);
            setLoading(false);
          });
        }, []);
        
        if (loading) return <p>Loading...</p>;
        if (!user) return <p>Not authenticated</p>;
        
        return <Component user={user} {...props} />;
      };
    };
    
    const Dashboard = ({ user }) => <h1>Welcome {user.name}</h1>;
    const ProtectedDashboard = withAuth(Dashboard);
  • HOCs add props, logic, or wrappers to existing components.
  • They're useful for cross-cutting concerns like authentication or theming.
  • Avoid deeply nested HOCs — use composition instead.

When to Use Each Pattern

  • Use compound components when building a cohesive component suite.
    <Form>
      <Form.Field name="email" />
      <Form.Field name="password" />
      <Form.Submit>Login</Form.Submit>
    </Form>
  • Use render props to share logic between unrelated components.
    <DataFetcher url="/api/users" render={data => (
      <UserList users={data} />
    )} />
  • Use HOCs to enhance existing components with logic.
    const enhancedComponent = withDataFetching(MyComponent);
  • Prefer composition over deeply nested patterns for readability.

Modern Alternatives

  • Custom hooks often replace render props and HOCs for sharing logic.
    function useMousePosition() {
      const [pos, setPos] = useState({ x: 0, y: 0 });
      useEffect(() => {
        const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
        window.addEventListener('mousemove', handleMove);
        return () => window.removeEventListener('mousemove', handleMove);
      }, []);
      return pos;
    }
    
    function MyComponent() {
      const pos = useMousePosition();
      return <p>Position: {pos.x}, {pos.y}</p>;
    }
  • Custom hooks are simpler and more flexible than render props or HOCs.
  • Use hooks as your first choice for sharing logic in modern React.
  • Composition patterns are still useful for component APIs and layout.

FAQ