Understanding React Contexts (Light/Dark Mode Example)
React Contexts let you share data across deeply nested components without having to pass props down multiple levels. They are ideal for global or page-wide state such as themes, authentication, or language settings.
1) Imports
import { createContext, useContext } from "react";
We typically work with two main pieces when using context:
- The Context – defines the “channel” through which data is shared.
- The Provider – wraps components and supplies the data to that channel.
This avoids “prop drilling”, where props have to be manually passed down through many layers of components.
2) The Context Example: ThemeContext.jsx
import { createContext, useContext } from "react";
export const ThemeContext = createContext(null);
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx; // { theme, toggleTheme }
}
The createContext(null) call sets up the context channel.
We initialise it with null so that if a component tries to use it outside of a provider,
the custom useTheme() hook will throw an error.
Some developers instead provide a default object for autocomplete support:
createContext({ theme: "light", toggleTheme: () => {} }),
but this example enforces correct usage strictly through the provider.
3) The Provider Example: ThemeProvider.jsx
import { useEffect, useMemo, useRef, useState } from "react";
import { ThemeContext } from "./ThemeContext";
const STORAGE_KEY = "app_theme"; // "light" | "dark"
function getInitialTheme() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === "light" || saved === "dark") return saved;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export default function ThemeProvider({ children }) {
const [theme, setTheme] = useState(getInitialTheme);
const rootRef = useRef(null);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, theme);
if (rootRef.current) rootRef.current.setAttribute("data-theme", theme);
}, [theme]);
const value = useMemo(
() => ({
theme,
toggleTheme: () => setTheme(t => (t === "light" ? "dark" : "light")),
}),
[theme]
);
return (
<ThemeContext.Provider value={value}>
<div ref={rootRef} className="context-demo-theme" data-theme={theme}>
{children}
</div>
</ThemeContext.Provider>
);
}
- getInitialTheme() checks
localStorageand the user’s system preference. - useRef stores a reference to the wrapper
<div>so we can update itsdata-themeattribute for CSS styling. - useMemo ensures the
valueobject only changes whenthemechanges, avoiding unnecessary re-renders.
This provider keeps the theme scoped to this page only.
A global app theme provider would normally attach data-theme to <html> or <body> instead.
4) Using the Provider: ContextDemo.jsx
import ThemeProvider from "../theme/ThemeProvider";
import ThemedButton from "../components/ThemedButton";
import "./ContextDemo.css";
export default function ContextDemo() {
return (
<ThemeProvider>
<main className="app">
<h1>Context Demo (page-scoped theme)</h1>
<ThemedButton />
<p>Only this page changes theme. Navigate elsewhere and it won’t be dark.</p>
</main>
</ThemeProvider>
);
}
Here, ThemeProvider wraps the page content, allowing any child (like ThemedButton) to access and modify the theme.
5) Consuming the Context: ThemedButton.jsx
import { useTheme } from "../theme/ThemeContext";
export default function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme} (click to toggle)
</button>
);
}
- The
useTheme()hook exposes the context value returned by the provider. themeis either"light"or"dark".toggleThemeflips the mode and persists it tolocalStorage.
6) Styling: ContextDemo.css
.context-demo-theme[data-theme="light"] {
--bg: white;
--fg: black;
}
.context-demo-theme[data-theme="dark"] {
--bg: #222;
--fg: white;
}
.context-demo-theme {
background-color: var(--bg);
color: var(--fg);
min-height: 100vh;
transition: all 0.3s ease;
}
The data-theme attribute enables scoped theming through CSS variables.
When the user toggles the theme, only elements inside this provider update their appearance.
✅ Summary
- createContext defines the shared data channel.
- Provider supplies data and functions to that channel.
- useContext (wrapped in a custom hook) allows child components to consume it easily.
- useMemo and useEffect manage performance and persistence.
- Ref + data attributes provide CSS hooks for styling.
This example shows how Context can elegantly manage cross-component state such as dark/light mode — locally or globally — with minimal prop passing.

















