Technology · React

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.

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.

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

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

React Conditional Rendering