- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Context Patterns
Context
Context Patterns
Exporting the raw context object makes every consumer repeat the same three lines. Wrapping it in a provider component and a hook is the pattern almost every codebase converges on.
Provider plus hook
Two exports - a provider component that owns the state, and a hook that reads it and throws a useful error when used outside. Consumers never touch createContext or useContext directly.
The standard shape
jsx
const ThemeContext = React.createContext(null)
// 1. The provider owns the state.
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState("light")
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"))
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
// 2. The hook hides useContext and fails loudly if misused.
function useTheme() {
const value = React.useContext(ThemeContext)
if (value === null) throw new Error("useTheme must be used inside a ThemeProvider")
return value
}
function Toolbar() {
const { theme, toggle } = useTheme()
return <button onClick={toggle}>Theme: {theme} (click to change)</button>
}
function App() {
return (
<ThemeProvider>
<Toolbar />
</ThemeProvider>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)The thrown error matters more than it looks. Without it, a component used outside the provider receives null and fails somewhere later with cannot read property of null - far from the actual mistake.
The new-object problem
value={{ theme, toggle }} creates a new object on every render of the provider. Context compares by identity, so every consumer re-renders even when nothing inside it changed.
Watch the consumer re-render
jsx
const Ctx = React.createContext(null)
function Consumer() {
const value = React.useContext(Ctx)
const renders = React.useRef(0)
renders.current++
return <p>Consumer rendered {renders.current} times (value: {value.label})</p>
}
function App() {
const [tick, setTick] = React.useState(0)
// A new object every render, even though label never changes.
const value = { label: "stable text" }
return (
<Ctx.Provider value={value}>
<button onClick={() => setTick(tick + 1)}>Re-render provider ({tick})</button>
<Consumer />
<p style={{ color: "#64748b" }}>
The label never changes, yet the consumer re-renders every time.
</p>
</Ctx.Provider>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)The fix is useMemo around the value, so the object keeps its identity until its parts actually change. That is covered properly in the performance section - the point here is knowing why it is needed.
Splitting a context in two
When one context holds a value that changes often and a setter that never changes, every consumer of the setter re-renders whenever the value moves - even ones that only dispatch.
Splitting them into two contexts, one for the value and one for the updater, means a component that only needs to trigger changes never re-renders when the value changes. It is a real technique, and also a sign you may want a reducer.
- Export a provider and a hook, not the context object.
- Throw from the hook when there is no provider.
- Memoise the value or every consumer re-renders.
- Split value and updater when consumers need only one of them.
