React - Context API (Advanced)

Goal

Use advanced Context patterns to minimize re-renders and scale provider design: split value/dispatch contexts, memoize values, and structure multiple small contexts.

When to use these patterns

  • You notice broad re-renders when a provider value changes.
  • You want stable dispatch functions and to avoid passing new objects every render.
  • You can split one big context into smaller, focused contexts for clarity and performance.

Try it: Split value and dispatch contexts

View source
const { createContext, useContext, useReducer, useMemo } = React;

const ThemeValueContext = createContext('light');
const ThemeDispatchContext = createContext(() => {});

function reducer(state, action){
  switch(action.type){
    case 'toggle': return state === 'light' ? 'dark' : 'light';
    default: return state;
  }
}

function ThemeProvider({ children }){
  const [theme, dispatch] = useReducer(reducer, 'light');
  // value changes when theme changes; dispatch is stable
  const value = theme; // primitive is fine; could also memoize object
  const dispatchStable = useMemo(() => dispatch, [dispatch]);
  return (
    <ThemeDispatchContext.Provider value={dispatchStable}>
      <ThemeValueContext.Provider value={value}>
        {children}
      </ThemeValueContext.Provider>
    </ThemeDispatchContext.Provider>
  );
}

function useTheme(){ return useContext(ThemeValueContext); }
function useThemeDispatch(){ return useContext(ThemeDispatchContext); }

function ToggleButton(){
  const dispatch = useThemeDispatch();
  return <button onClick={() => dispatch({ type:'toggle' })}>Toggle</button>;
}
function ThemedPanel(){
  const theme = useTheme();
  const style = theme === 'dark' ? { background:'#111827', color:'#e5e7eb', padding:10, borderRadius:8 } : { background:'#e5e7eb', color:'#111827', padding:10, borderRadius:8 };
  return <div style={style}>Theme: <strong>{theme}</strong></div>;
}

function App(){
  return (
    <ThemeProvider>
      <div style={{display:'flex', gap:8, alignItems:'center'}}>
        <ToggleButton />
        <ThemedPanel />
      </div>
    </ThemeProvider>
  );
}

ReactDOM.createRoot(document.getElementById('try-context-adv-split')).render(<App />);

Try it: Memoize provider value and use minimal contexts

View source
const { createContext, useContext, useMemo, useState } = React;

const UserNameContext = createContext('');
const UserRoleContext = createContext('guest');

function UserProvider({ children }){
  const [name, setName] = useState('Ada');
  const [role, setRole] = useState('admin');
  // Separate contexts so changing role doesn't re-render consumers that only read name
  const nameValue = useMemo(() => name, [name]);
  const roleValue = useMemo(() => role, [role]);
  return (
    <UserNameContext.Provider value={nameValue}>
      <UserRoleContext.Provider value={roleValue}>
        {children}
      </UserRoleContext.Provider>
    </UserNameContext.Provider>
  );
}

function useUserName(){ return useContext(UserNameContext); }
function useUserRole(){ return useContext(UserRoleContext); }

function NameBadge(){ const name = useUserName(); return <span>Name: <strong>{name}</strong></span>; }
function RoleBadge(){ const role = useUserRole(); return <span>Role: <strong>{role}</strong></span>; }

function App(){
  const [name, setName] = React.useState('Ada');
  const [role, setRole] = React.useState('admin');
  return (
    <UserNameContext.Provider value={name}>
      <UserRoleContext.Provider value={role}>
        <div style={{display:'flex', gap:10, alignItems:'center'}}>
          <NameBadge />
          <RoleBadge />
        </div>
        <div style={{marginTop:10, display:'flex', gap:8}}>
          <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
          <select value={role} onChange={e => setRole(e.target.value)}>
            <option value="admin">admin</option>
            <option value="user">user</option>
          </select>
        </div>
      </UserRoleContext.Provider>
    </UserNameContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById('try-context-adv-memo')).render(<App />);

Notes and pitfalls

  • Value identity matters: avoid passing a new object each render unless it actually changed. Wrap objects in useMemo.
  • Split contexts by concern (value vs dispatch, or separate fields). Consumers only re-render when the context they read changes.
  • Heavy, rapidly changing data may be better managed with state libraries or fine-grained context splits.