DNA⚛ïļ ReactComponent Design Patterns
ðŸĢHatchlingReactPatternsArchitecture

Component Design Patterns

Compound components, render props, HOCs — these aren't academic patterns. They're how you build flexible, reusable component APIs that scale.

Component Design Patterns

The patterns you choose for component APIs determine whether your codebase scales gracefully or collapses under its own weight. Senior engineers don't just know these patterns — they know when each one is the right choice.

Compound Components

Compound components share implicit state through a parent, giving consumers flexible composition:

const Select = ({ children, onChange }: SelectProps) => {
  const [value, setValue] = useState<string | null>(null);
 
  const handleSelect = (v: string) => {
    setValue(v);
    onChange?.(v);
  };
 
  return (
    <SelectContext.Provider value={{ value, onSelect: handleSelect }}>
      <div role="listbox">{children}</div>
    </SelectContext.Provider>
  );
};
 
const Option = ({ value, children }: OptionProps) => {
  const ctx = useContext(SelectContext);
  const selected = ctx.value === value;
 
  return (
    <div
      role="option"
      aria-selected={selected}
      onClick={() => ctx.onSelect(value)}
    >
      {children}
    </div>
  );
};
 
Select.Option = Option;
 
// Usage — consumers control layout and composition
<Select onChange={handleChange}>
  <Select.Option value="a">Alpha</Select.Option>
  <Select.Option value="b">Beta</Select.Option>
</Select>

When to use: Component libraries, form controls, navigation — anywhere users need to compose children flexibly while sharing state.

Render Props

A component that takes a function as a child (or prop) and delegates rendering to the consumer:

function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
 
  return <>{render(pos)}</>;
}
 
// Usage
<MouseTracker render={({ x, y }) => <Tooltip x={x} y={y} />} />

Render props were the go-to pattern before hooks. They're still valuable when you need inversion of control — letting the consumer decide what to render with the data.

Modern Alternative: Custom Hooks

Most render prop use cases are now better served by hooks:

function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
  return pos;
}

Use render props when you need to share behavior that includes JSX structure, not just data.

Higher-Order Components (HOCs)

A function that takes a component and returns an enhanced component:

function withAuth<P extends object>(Component: ComponentType<P>) {
  return function AuthenticatedComponent(props: P) {
    const { user, loading } = useAuth();
    if (loading) return <Spinner />;
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} />;
  };
}
 
const ProtectedDashboard = withAuth(Dashboard);

HOC pitfalls:

  • Props get swallowed or collide (the "wrapper hell" problem)
  • Debugging is harder — component names in DevTools are opaque
  • Static methods and refs don't forward automatically

When HOCs still make sense: Cross-cutting concerns applied to many components (auth guards, error boundaries, analytics wrappers), especially when the concern includes conditional rendering.

Composition Over Inheritance

React explicitly favors composition. The children prop is the simplest composition tool:

function Card({ children }: { children: ReactNode }) {
  return <div className="card">{children}</div>;
}
 
function CardWithHeader({ title, children }: { title: string; children: ReactNode }) {
  return (
    <Card>
      <h2>{title}</h2>
      <div>{children}</div>
    </Card>
  );
}

Slot Pattern

For more complex layouts, use named slots via props:

function Layout({ sidebar, header, children }: LayoutProps) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}
 
<Layout
  header={<Nav />}
  sidebar={<Menu />}
>
  <Content />
</Layout>

Pattern Selection Guide

PatternUse whenAvoid when
Compound componentsFlexible composition with shared stateSimple, non-composable UI
Render propsConsumer controls rendering with shared logicA hook would suffice
HOCsCross-cutting concerns across many componentsOnly 1-2 components need it
Composition (children/slots)Layout and structural patternsState sharing is needed

Interview Signal

Senior engineers demonstrate pattern literacy by explaining trade-offs, not just implementations. The answer to "which pattern?" is always "it depends" — followed by concrete criteria for the decision.