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 };
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.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]).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 |