1. Introduction
As React applications grow, the same architectural questions keep resurfacing: how should components share logic? how should state be organized? how do we keep the codebase maintainable at scale? Over the years, the community has converged on a set of proven design patterns that answer these questions.
This tutorial catalogs the most important React patterns â from compound components to the state reducer pattern â along with organizational and performance best practices for production codebases.
Information
2. Why Design Patterns Matter đ¤
- Shared vocabulary â naming a pattern lets teams communicate intent quickly ("let's use a render prop here").
- Proven solutions â patterns emerged from real problems, so adopting one avoids reinventing (and re-debugging) the same wheel.
- Consistency â a shared set of patterns makes unfamiliar code easier to navigate across a large codebase.
Caution
3. Component Composition đ§Š
Composition is React's core philosophy: build complex UIs by combining small, focused components rather than configuring one large component with many props.
Composition over configuration
// Prefer this:
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
</Card>
// Over this:
<Card
title="Title"
content="Content"
showHeader={true}
headerStyle="bold"
/>Tip
4. Compound Components Pattern đ§Ŧ
Compound components work together as a set, sharing implicit state via Context, while giving consumers full control over layout and composition â similar to how native <select> and <option> work together.
Compound Tabs example
const TabsContext = createContext(null);
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
{children}
</TabsContext.Provider>
);
}
Tabs.List = function TabList({ children }) {
return <div className="tab-list">{children}</div>;
};
Tabs.Tab = function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
<button
className={activeIndex === index ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
};Best Practice
5. Higher-Order Components (HOC) đ
A Higher-Order Component is a function that takes a component and returns a new, enhanced component â a pattern for reusing cross-cutting logic like authentication checks or data fetching.
withAuth HOC
function withAuth(Component) {
return function AuthenticatedComponent(props) {
const { user } = useAuth();
if (!user) return <LoginPrompt />;
return <Component {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);Note
6. Render Props Pattern đ
The render props pattern shares logic by passing a function as a prop (often named render or simply children), which the component calls with internal state or data.
MouseTracker with render props
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return children(position);
}
// Usage
<MouseTracker>
{({ x, y }) => <p>Mouse at {x}, {y}</p>}
</MouseTracker>;Note
7. Custom Hooks Pattern đĒ
Custom hooks are the modern, preferred way to extract and reuse stateful logic across components â no wrapping, no nesting, just a function starting with use.
useWindowSize.ts
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage: flat, no extra nesting
function App() {
const { width } = useWindowSize();
return <p>Width: {width}</p>;
}Best Practice
8. Provider Pattern đ§ĩ
The Provider pattern uses Context to make data or functions available to an entire subtree without manually passing props through every intermediate level ("prop drilling").
Provider pattern
const CartContext = createContext(null);
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (item) => setItems((prev) => [...prev, item]);
return (
<CartContext.Provider value={{ items, addItem }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
}Warning
9. Controlled Components Pattern đī¸
A controlled component has its value fully driven by React state â the source of truth lives in the parent, and the component reflects it via value and onChange.
Controlled input
function ControlledInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}Tip
10. Uncontrolled Components Pattern đ
An uncontrolled component manages its own internal state via the DOM, with React reading the value on demand â typically via a ref â instead of tracking every change.
Uncontrolled input
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={handleSubmit}>Submit</button>
</>
);
}Note
11. Container and Presentational Pattern đĻ
This older pattern separates data/logic (container) from markup/styling (presentational). Containers fetch data and manage state; presentational components simply render props they're given.
Container vs. Presentational
// Presentational: pure, no logic
function UserList({ users }) {
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Container: owns data fetching
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return <UserList users={users} />;
}Note
12. Headless Components Pattern đŠ
Headless components (or headless hooks) provide behavior and state without any markup or styling at all, leaving 100% of the visual presentation to the consumer â popular in libraries like Radix and downshift.
Headless useToggle
function useDisclosure(initial = false) {
const [isOpen, setIsOpen] = useState(initial);
return {
isOpen,
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen((v) => !v),
};
}
// Consumer supplies 100% of the markup
function Modal() {
const { isOpen, open, close } = useDisclosure();
return (
<>
<button onClick={open}>Open</button>
{isOpen && <div className="my-custom-modal">...<button onClick={close}>Ã</button></div>}
</>
);
}13. Layout Components Pattern đ
Layout components encapsulate structural/spacing concerns (grids, stacks, containers) as reusable primitives, keeping page-level components focused on content rather than CSS.
Stack layout primitive
function Stack({ gap = '1rem', children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap }}>
{children}
</div>
);
}
// Usage
<Stack gap="2rem">
<Header />
<MainContent />
<Footer />
</Stack>;14. Slots Pattern đ°
The slots pattern lets a component expose named insertion points for content, giving consumers control over specific regions without needing to reimplement the whole layout.
Named slots via props
function PageLayout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Usage
<PageLayout header={<Nav />} sidebar={<FilterPanel />}>
<ProductGrid />
</PageLayout>;Tip
15. State Reducer Pattern đ
The state reducer pattern lets a component's consumer control how internal state transitions happen, by intercepting and optionally overriding the reducer's decisions.
State reducer pattern
function toggleReducer(state, action) {
switch (action.type) {
case 'toggle':
return { on: !state.on };
default:
return state;
}
}
function useToggle({ reducer = toggleReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { on: false });
const toggle = () => dispatch({ type: 'toggle' });
return { on: state.on, toggle };
}
// Consumer overrides behavior: prevent toggling off once on
function myReducer(state, action) {
if (action.type === 'toggle' && state.on) return state; // ignore
return toggleReducer(state, action);
}
const { on, toggle } = useToggle({ reducer: myReducer });Important
16. Props Getter Pattern đ§ˇ
The props getter pattern returns pre-composed prop objects (event handlers, ARIA attributes, etc.) from a hook, ensuring consumers can't accidentally forget to wire up required accessibility or behavior logic.
Props getter example
function useToggle() {
const [on, setOn] = useState(false);
function getTogglerProps(props = {}) {
return {
'aria-pressed': on,
onClick: () => {
props.onClick?.();
setOn((v) => !v);
},
...props,
};
}
return { on, getTogglerProps };
}
// Usage
const { on, getTogglerProps } = useToggle();
<button {...getTogglerProps({ onClick: () => console.log('clicked') })}>
{on ? 'ON' : 'OFF'}
</button>;17. Context Pattern đ§
Beyond simple providers, a well-structured Context pattern separates state and dispatch into two contexts, so components that only need to trigger updates don't re-render when state changes.
Split state/dispatch contexts
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}18. Feature-Based Architecture đī¸
Feature-based architecture organizes code by domain/feature rather than by technical type (all components together, all hooks together). Each feature folder is largely self-contained.
Best Practice
19. Folder Structure đ
Regardless of architecture, a few conventions consistently help:
- Colocate a component's test, styles, and types next to the component file itself.
- Reserve a shared/ or common/ folder for truly cross-feature building blocks only.
- Avoid deeply nested folders â three or four levels is usually the practical ceiling before navigation becomes painful.
20. Component Organization đ§ą
Within a single component file, a consistent internal ordering makes files predictable to scan:
- Imports
- Type/interface definitions
- The component function: hooks first, then derived values, then handlers, then the returned JSX
- Any small helper functions used only by this component
21. Code Reusability âģī¸
Favor composition and custom hooks over copy-pasting logic. When extracting reusable code, prefer small, focused hooks and components over large, do-everything abstractions that are hard to customize.
Hint
22. State Management Best Practices đī¸
- Keep state as local as possible â lift it up only when multiple components genuinely need to share it.
- Distinguish server state (data from an API, with caching/staleness concerns) from client state (UI toggles, form input) â libraries like React Query or SWR handle the former far better than useState.
- Avoid duplicating state that can be derived from existing state or props during render.
- Reach for a global store (Redux, Zustand, Context) only once prop drilling becomes a genuine pain point.
23. Performance Best Practices đ
- Memoize expensive computations with useMemo, and stable callback references with useCallback, only where profiling shows a real benefit.
- Use React.memo for components that re-render often with unchanged props.
- Virtualize long lists (e.g. with react-window) instead of rendering thousands of DOM nodes at once.
- Code-split routes and heavy components with React.lazy and Suspense.
Warning
24. Accessibility Best Practices âŋ
- Use semantic HTML elements (<button>, <nav>) instead of styled <div>s with click handlers.
- Ensure every interactive element is reachable and operable via keyboard alone.
- Provide accessible names via <label>, aria-label, or visible text â never rely on placeholder text alone.
- Manage focus explicitly when opening modals or navigating between views.
25. Security Best Practices đ
- Never render unsanitized user input with dangerouslySetInnerHTML â sanitize with a trusted library first if it's truly needed.
- Keep secrets (API keys, tokens) out of client-side code entirely; environment variables bundled into a React app are publicly visible.
- Validate and sanitize data on the server too â client-side checks are a UX convenience, not a security boundary.
26. Scalability Best Practices đ
- Establish clear boundaries between features so teams can work independently without stepping on each other's code.
- Adopt a consistent component API style (prop naming, event naming) across the codebase.
- Invest in a shared design-system / component library early â retrofitting consistency later is far more expensive.
27. Code Style Guidelines đ¨
- Enforce a linter (ESLint) and formatter (Prettier) via CI so style discussions don't happen in code review.
- Name components with PascalCase and hooks with a use prefix, following React's own conventions.
- Keep components small â if a file exceeds a few hundred lines, it's often a sign it should be split.
28. Common Anti-Patterns â
| Anti-Pattern | Why It Hurts |
|---|---|
| Prop drilling through 5+ levels | Makes refactoring fragile; use context or composition instead |
| Giant "God" components | Hard to test, reuse, or reason about in isolation |
| Mutating state directly | Breaks React's change-detection, causing missed re-renders |
| Deriving state with useEffect unnecessarily | Adds an extra render pass when the value could be computed directly during render |
29. Refactoring Strategies đ§
- Extract repeated JSX into a named sub-component before extracting shared logic into a hook.
- Introduce tests around a component before refactoring it, so behavior changes are caught immediately.
- Refactor incrementally â replace one small piece, verify, then continue, rather than rewriting a large component all at once.
Tip
30. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
31. Summary đ
Summary
The best pattern is always the simplest one that solves your actual problem â reach for more powerful abstractions only once real complexity demands them. Happy building! đ