React Context Patterns

Structure context providers for performance and clean consumption

Overview

Context solves prop drilling, but naive usage causes every consumer to re-render whenever any part of the value changes. Good patterns split state from dispatch, memoize the provider value, and expose a custom hook that guards against use outside the provider.

Syntax / Usage

Split read and write concerns into separate contexts so components only subscribe to what they need.

import { createContext, useContext, useMemo, useReducer } from 'react'

const StateContext = createContext(null)
const DispatchContext = createContext(null)

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'reset':
      return { count: 0 }
    default:
      return state
  }
}

export function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}

export function useCounterState() {
  const ctx = useContext(StateContext)
  if (ctx === null) throw new Error('useCounterState must be inside CounterProvider')
  return ctx
}

export function useCounterDispatch() {
  const ctx = useContext(DispatchContext)
  if (ctx === null) throw new Error('useCounterDispatch must be inside CounterProvider')
  return ctx
}

Examples

Memoize a combined value object so consumers don't re-render on unrelated parent updates:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const value = useMemo(() => ({ theme, setTheme }), [theme])
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

Components that only dispatch never re-render when state changes:

function ResetButton() {
  const dispatch = useCounterDispatch()
  return <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
}

Common Mistakes

  • Creating a new value object each render without useMemo, forcing all consumers to update
  • Storing rapidly changing values (like scroll position) in context
  • Putting the entire app state in one context instead of scoping providers
  • Skipping the null check, leading to cryptic errors when used outside a provider
  • Nesting many providers manually instead of composing a single wrapper component

See Also

use-context react-custom-hooks react-performance-optimization