RE

useContext

React · 6 entries

Creating Context

syntax
import { createContext } from 'react';
const MyContext = createContext(defaultValue);
example
// theme-context.js
import { createContext } from 'react';

export const ThemeContext = createContext({
  mode: 'light',
  toggleTheme: () => {},
});

Note The default value is used only when a component reads the context but there is no matching Provider above it in the tree. Pass a realistic shape as the default so TypeScript inference and fallback behavior work correctly.

Providing Context

syntax
<MyContext.Provider value={contextValue}>
  {children}
</MyContext.Provider>
example
import { useState } from 'react';
import { ThemeContext } from './theme-context';

function ThemeProvider({ children }) {
  const [mode, setMode] = useState('light');

  const contextValue = {
    mode,
    toggleTheme: () => setMode(m => m === 'light' ? 'dark' : 'light'),
  };

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Note Wrap the Provider at the highest point where descendant components need the value. If the value is a new object on every render, wrap it with useMemo to prevent unnecessary re-renders of consumers.

Consuming Context

syntax
import { useContext } from 'react';
const value = useContext(MyContext);
example
import { useContext } from 'react';
import { ThemeContext } from './theme-context';

function ThemeToggleButton() {
  const { mode, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: mode === 'dark' ? '#333' : '#fff',
        color: mode === 'dark' ? '#fff' : '#333',
      }}
    >
      Current: {mode}
    </button>
  );
}

Note useContext always looks for the nearest Provider above the calling component. If none is found, it uses the default value from createContext. The component re-renders whenever the provided value changes.

Multiple Contexts

syntax
<AuthContext.Provider value={auth}>
  <ThemeContext.Provider value={theme}>
    {children}
  </ThemeContext.Provider>
</AuthContext.Provider>
example
function AppProviders({ children }) {
  const auth = useAuthState();
  const theme = useThemeState();
  const locale = useLocaleState();

  return (
    <AuthContext.Provider value={auth}>
      <ThemeContext.Provider value={theme}>
        <LocaleContext.Provider value={locale}>
          {children}
        </LocaleContext.Provider>
      </ThemeContext.Provider>
    </AuthContext.Provider>
  );
}

// Usage at app root
<AppProviders>
  <App />
</AppProviders>

Note Split unrelated data into separate contexts. A single monolithic context forces every consumer to re-render when any part of the value changes, even if they only use a small slice.

Context with State (Custom Hook)

syntax
// Encapsulate context + hook in a module
const Ctx = createContext(null);
export function useMyContext() {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error('Missing Provider');
  return ctx;
}
example
import { createContext, useContext, useState } from 'react';

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  function addItem(product) {
    setItems(prev => [...prev, { ...product, qty: 1 }]);
  }

  function removeItem(id) {
    setItems(prev => prev.filter(item => item.id !== id));
  }

  const totalItems = items.reduce((sum, i) => sum + i.qty, 0);

  return (
    <CartContext.Provider value={{ items, addItem, removeItem, totalItems }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
}

Note Exporting a custom hook that throws when the Provider is missing catches mistakes early and gives a clear error message instead of undefined values that silently break downstream.

Avoiding Unnecessary Re-renders

syntax
// Memoize the value object
const value = useMemo(() => ({ data, handler }), [data]);

// Split read-only data from dispatch
<DataContext.Provider value={data}>
  <DispatchContext.Provider value={dispatch}>
example
import { createContext, useContext, useMemo, useState } from 'react';

const CountContext = createContext(null);
const CountDispatchContext = createContext(null);

function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  // dispatch never changes identity
  const increment = useMemo(
    () => () => setCount(c => c + 1),
    []
  );

  return (
    <CountContext.Provider value={count}>
      <CountDispatchContext.Provider value={increment}>
        {children}
      </CountDispatchContext.Provider>
    </CountContext.Provider>
  );
}

// Components reading only dispatch won't re-render when count changes
function IncrementButton() {
  const increment = useContext(CountDispatchContext);
  return <button onClick={increment}>+1</button>;
}

Note Splitting context into a data context and a dispatch context is a powerful pattern. Components that only call actions (dispatch) will not re-render when the data changes, because the dispatch reference stays stable.