Skip to content
React rc patterns 4 min read

Component Patterns Overview

React gives you a tiny API surface — components, props, state, and effects — but says almost nothing about how to organize larger features. Component patterns are the conventions the community has converged on for sharing logic, composing UI, and keeping components decoupled as a codebase grows. None of them are framework features; they are disciplined ways of combining the primitives you already have. This page surveys the patterns you will reach for most and explains when each one earns its keep.

Why patterns matter

A single component is easy. The trouble starts when three screens need the same dropdown behavior, when a form must coordinate validation across fields, or when business logic creeps into files that should only render markup. Without a shared vocabulary, teams reinvent solutions inconsistently and the codebase drifts.

Patterns solve recurring problems with predictable structure. They make code easier to read because a teammate recognizes the shape immediately, and easier to test because responsibilities are separated. The goal is never to apply every pattern — it is to recognize which problem you actually have and pick the lightest tool that fixes it.

Composition over configuration

The most fundamental React pattern is composition: building complex UI by nesting small components and passing other components through children rather than piling up configuration props. When a component starts sprouting boolean flags like showHeader, showFooter, and variant, composition is usually the cure.

function Card({ children }) {
  return <section className="card">{children}</section>;
}

function CardHeader({ children }) {
  return <header className="card__header">{children}</header>;
}

function CardBody({ children }) {
  return <div className="card__body">{children}</div>;
}

export default function ProductCard() {
  return (
    <Card>
      <CardHeader>Wireless Headphones</CardHeader>
      <CardBody>Noise cancelling, 30-hour battery.</CardBody>
    </Card>
  );
}

The caller decides the structure. Card never has to anticipate every layout because it simply renders whatever it is given.

Tip: Reach for the children prop before inventing a new configuration flag. Most “should this component show X?” questions are really “let the parent compose X.”

The pattern landscape

Each pattern below addresses a distinct concern. The table summarizes the trade-offs; the dedicated pages go deeper with runnable examples.

PatternProblem it solvesBest whenWatch out for
Compound componentsCoordinating sibling parts of one widgetTabs, menus, accordions with shared stateImplicit coupling via context
Render propsSharing behavior while leaving rendering to the callerHighly variable output from shared logicNesting and “wrapper hell”
Higher-order componentsWrapping components to inject props or behaviorCross-cutting concerns in legacy codeProp collisions, opaque stacks
Custom hooksReusing stateful logic without changing the treeAlmost any shared logic in modern ReactHiding too much in one hook
Container / presentationalSeparating data fetching from renderingClear test seams and reusable viewsOver-splitting trivial components
ProviderSharing global-ish state without prop drillingTheme, auth, locale, feature flagsRe-renders from oversized context

Custom hooks: the modern default

Since hooks arrived, most logic that once needed render props or HOCs is now expressed as a custom hook. A hook is just a function that calls other hooks, so it can hold state and effects while returning plain values.

import { useState, useEffect } from "react";

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  return width;
}

export default function Layout() {
  const width = useWindowWidth();
  return <p>{width < 768 ? "Mobile layout" : "Desktop layout"}</p>;
}

Any component can consume useWindowWidth without changing its JSX structure — no wrapper components, no extra nesting. This is why hooks displaced the older logic-sharing patterns for most new code.

Sharing data without prop drilling

When many components at different depths need the same value, threading it through every prop becomes painful. The provider pattern lifts that value into a React context and lets descendants read it directly.

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

const ThemeContext = createContext("light");

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

Output:

A button deep in the tree can call useTheme() to read and flip the theme,
with no intermediate component forwarding a single prop.

Choosing a pattern

Start from the problem, not the pattern. If you need to share logic, write a custom hook. If you need to coordinate parts of one widget, use compound components. If you need to share state across the tree, use a provider. Render props and HOCs remain useful, especially in libraries and older code, but they are no longer the first choice for everyday logic reuse.

Best practices

  • Prefer composition with children before adding configuration props or new variants.
  • Reach for custom hooks first when sharing stateful logic in modern React.
  • Keep context values small and split unrelated state into separate providers to limit re-renders.
  • Use compound components only when sibling parts genuinely share state; otherwise plain props are clearer.
  • Treat HOCs as a legacy or interop tool, and document any props they inject to avoid silent collisions.
  • Don’t apply a pattern preemptively — wait until duplication or coupling actually appears.
Last updated June 14, 2026
Was this helpful?