React Patterns & Best Practices đŸ›ī¸

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

Patterns are tools, not rules. The goal is always to reach for the simplest pattern that solves the problem at hand.

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

Overusing patterns for simple problems adds unnecessary abstraction. A pattern should reduce complexity, not just relocate it.

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

When you find yourself adding boolean flags to control what a component renders, that's usually a sign composition would serve you better.

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

Compound components shine when a parent needs to coordinate several closely related children without prop-drilling that state manually through every level.

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

HOCs were the primary logic-reuse pattern before hooks existed. They're still valid but have largely been superseded by custom hooks, which avoid HOCs' "wrapper hell" and prop name collisions.

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

Like HOCs, render props predate hooks and have largely been replaced by custom hooks for sharing stateful logic, since hooks avoid the extra nesting level render props introduce.

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

If you notice the same useState + useEffect combination appearing in multiple components, that's a strong signal to extract a custom hook.

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

Every consumer of a context re-renders when its value changes. For frequently updating values, split contexts by concern or pair the pattern with a state management library.

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

Controlled components make validation, formatting, and conditional logic straightforward, since every keystroke passes through your state update logic.

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

Uncontrolled components involve less re-rendering and can be simpler for basic forms, but they sacrifice fine-grained control like live validation on every keystroke.

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

With hooks, this separation is often achieved more simply by extracting a custom hook (e.g. useUsers()) rather than splitting into two components.

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

Slots are essentially named props holding JSX — a lightweight alternative to full compound components when you only need a handful of insertion points.

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

This pattern gives maximum flexibility to consumers of a reusable hook/component without forcing you to anticipate every possible customization as a separate prop.

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.

src
features
auth
LoginForm.tsx
useAuth.ts
authApi.ts

Best Practice

Feature-based structure scales much better than type-based structure ("all components in one folder") as an app grows — related code stays physically close together.

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:

  1. Imports
  2. Type/interface definitions
  3. The component function: hooks first, then derived values, then handlers, then the returned JSX
  4. 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

A good rule of thumb: if you're copying a block of logic for the third time, that's the moment to extract it — extracting after the first use often produces the wrong abstraction.

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 🚀

  1. Memoize expensive computations with useMemo, and stable callback references with useCallback, only where profiling shows a real benefit.
  2. Use React.memo for components that re-render often with unchanged props.
  3. Virtualize long lists (e.g. with react-window) instead of rendering thousands of DOM nodes at once.
  4. Code-split routes and heavy components with React.lazy and Suspense.

Warning

Don't reach for useMemo/useCallback everywhere by default — they have their own overhead and can make code harder to read without measurable benefit.

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-PatternWhy It Hurts
Prop drilling through 5+ levelsMakes refactoring fragile; use context or composition instead
Giant "God" componentsHard to test, reuse, or reason about in isolation
Mutating state directlyBreaks React's change-detection, causing missed re-renders
Deriving state with useEffect unnecessarilyAdds an extra render pass when the value could be computed directly during render

29. Refactoring Strategies 🔧

  1. Extract repeated JSX into a named sub-component before extracting shared logic into a hook.
  2. Introduce tests around a component before refactoring it, so behavior changes are caught immediately.
  3. Refactor incrementally — replace one small piece, verify, then continue, rather than rewriting a large component all at once.

Tip

When a component has grown unwieldy, first look for pieces that can become compound components or be lifted into custom hooks — that alone often resolves most of the complexity.

30. Frequently Asked Questions ❓

Question

Are HOCs and render props obsolete now that hooks exist?

Answer

Mostly, yes, for logic reuse — custom hooks handle that more cleanly. HOCs and render props still appear in some libraries and specific cases (e.g. wrapping non-hook class components).

Question

When should I reach for Redux instead of Context?

Answer

When state updates are frequent and shared across many components, or when you need powerful devtools, middleware, and time-travel debugging — Context alone can cause broad re-renders and lacks built-in optimization for high-frequency updates.

Question

Is the container/presentational split still relevant?

Answer

Less so today — custom hooks usually achieve the same separation of concerns more simply, without needing two separate component files.

31. Summary 📌

Summary

React's pattern landscape has evolved from class-era techniques like HOCs and render props toward custom hooks as the primary tool for logic reuse, while compound components, the provider pattern, and headless components remain valuable for building flexible, reusable UI. Pairing these patterns with sound folder structure, state management, and performance practices is what lets a React codebase scale gracefully.

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! 🎉