stackademic

The leading education platform for anyone with an interest in software development.

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