Creating a Context and Provider Component

Building a context involves three pieces working together: createContext creates the context object, a Provider component owns the state and wraps the tree, and a custom hook gives consumers clean, safe access to the value. In this lesson you will build the complete pattern from scratch โ€” including the safeguard that throws a helpful error if the hook is used outside a Provider โ€” and understand the performance implications of what you put in the context value object.

The Three Parts of Context

// Part 1: Create the context (usually in the same file as the Provider)
const ThemeContext = createContext(undefined);
// undefined as default: makes it detectable if used outside a Provider

// Part 2: Provider component โ€” owns the state, wraps the tree
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');

  const toggleTheme = () => {
    setTheme(prev => {
      const next = prev === 'light' ? 'dark' : 'light';
      localStorage.setItem('theme', next);
      document.documentElement.setAttribute('data-theme', next);
      return next;
    });
  };

  // The value object: everything consumers can read or call
  const value = { theme, toggleTheme };

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

// Part 3: Custom hook โ€” safe, typed access with helpful error
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

export { ThemeProvider, useTheme };
Note: Always initialise createContext with undefined (not null or an empty object). Your custom hook then checks for undefined and throws a descriptive error if it is called outside the Provider. This surfaces the bug immediately with a clear message โ€” much better than the silent failures or cryptic “cannot read property of null” errors you get when the default value is an empty object.
Tip: The context value prop should be a stable object. If you write value={{ theme, toggleTheme }} directly inside the render, a new object is created on every render of the Provider โ€” this causes all consumers to re-render even when theme and toggleTheme have not changed. Wrap the value in useMemo when the Provider renders frequently: const value = useMemo(() => ({ theme, toggleTheme }), [theme]).
Warning: Do not create more than one instance of a context in your app. createContext should be called once at module level, not inside a component or hook. If two components each call createContext() for what should be the same data, they create two separate, independent contexts โ€” consumers of one will not see values from the other.

Mounting the Provider in main.jsx

// src/main.jsx โ€” providers wrap the app in order of dependency
import { StrictMode }    from 'react';
import { createRoot }    from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from '@/context/ThemeContext';
import { AuthProvider }  from '@/context/AuthContext';  // covered in Lesson 3
import App               from './App.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <ThemeProvider>
        <AuthProvider>
          {/* AuthProvider is inside ThemeProvider so it can use useTheme if needed */}
          <App />
        </AuthProvider>
      </ThemeProvider>
    </BrowserRouter>
  </StrictMode>
);

Performance โ€” Memoising the Context Value

import { createContext, useContext, useState, useMemo, useCallback } from 'react';

const ThemeContext = createContext(undefined);

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

  // Stable function reference โ€” does not change between renders
  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []); // no dependencies โ€” toggleTheme is truly stable

  // Memoised value โ€” only changes when theme changes
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
  // Now: consumers only re-render when theme actually changes
  // Not: every time ThemeProvider's parent re-renders

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

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
  return ctx;
}

export { ThemeProvider, useTheme };

A Minimal Context โ€” Step by Step

// Step 1: File structure
// src/context/ThemeContext.jsx  โ€” context + provider + hook

// Step 2: Imports
import { createContext, useContext, useState } from 'react';

// Step 3: Create context (module level โ€” once)
const ThemeContext = createContext(undefined);

// Step 4: Provider โ€” owns state, exposes value
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Step 5: Custom hook โ€” safe consumption
export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
  return ctx;
}

// Step 6: Use in a component
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      {theme === 'light' ? '๐ŸŒ™ Dark' : 'โ˜€๏ธ Light'}
    </button>
  );
}

Common Mistakes

Mistake 1 โ€” Not memoising the context value

โŒ Wrong โ€” new object on every render causes all consumers to re-render:

return (
  <ThemeContext.Provider value={{ theme, toggleTheme }}> {/* new object every render! */}
    {children}
  </ThemeContext.Provider>
);

โœ… Correct โ€” memoise so consumers only re-render when the value changes:

const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; // โœ“

Mistake 2 โ€” Calling createContext inside a component

โŒ Wrong โ€” new context created on every render โ€” consumers can never match the context:

function ThemeProvider({ children }) {
  const ThemeContext = createContext(undefined); // NEW context every render!
  return <ThemeContext.Provider value={...}>{children}</ThemeContext.Provider>;
}

โœ… Correct โ€” createContext at module level, once:

const ThemeContext = createContext(undefined); // โœ“ module-level constant

function ThemeProvider({ children }) { ... }

Mistake 3 โ€” Not throwing in the custom hook

โŒ Wrong โ€” silent failure when used outside Provider:

function useTheme() {
  return useContext(ThemeContext); // returns undefined if no Provider โ€” no error
}

โœ… Correct โ€” throw a helpful error:

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be inside ThemeProvider'); // โœ“
  return ctx;
}

Quick Reference

Step Code
Create context const Ctx = createContext(undefined) โ€” module level
Provide value <Ctx.Provider value={memoValue}>{children}</Ctx.Provider>
Consume safely const ctx = useContext(Ctx); if (!ctx) throw new Error(...)
Stable functions Wrap with useCallback(fn, deps)
Stable value object Wrap with useMemo(() => ({ ... }), [deps])
Mount Provider Wrap App (or subtree) in Provider in main.jsx

🧠 Test Yourself

You initialise your ThemeContext with createContext({}) instead of createContext(undefined). Your custom hook does not check for undefined. What happens if a component uses useTheme() without a ThemeProvider in its ancestor tree?