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
childrenprop 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.
| Pattern | Problem it solves | Best when | Watch out for |
|---|---|---|---|
| Compound components | Coordinating sibling parts of one widget | Tabs, menus, accordions with shared state | Implicit coupling via context |
| Render props | Sharing behavior while leaving rendering to the caller | Highly variable output from shared logic | Nesting and “wrapper hell” |
| Higher-order components | Wrapping components to inject props or behavior | Cross-cutting concerns in legacy code | Prop collisions, opaque stacks |
| Custom hooks | Reusing stateful logic without changing the tree | Almost any shared logic in modern React | Hiding too much in one hook |
| Container / presentational | Separating data fetching from rendering | Clear test seams and reusable views | Over-splitting trivial components |
| Provider | Sharing global-ish state without prop drilling | Theme, auth, locale, feature flags | Re-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
childrenbefore 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.