How it works
The explanation, with examples you can run.
Providing and reading
import { createContext, useContext, useMemo, useState, type ReactNode } from "react" type Theme = "light" | "dark"type ThemeValue = { theme: Theme; toggle: () => void } const ThemeContext = createContext<ThemeValue | null>(null) export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>("light") const value = useMemo( () => ({ theme, toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")) }), [theme], ) return <ThemeContext value={value}>{children}</ThemeContext>} export function useTheme() { const value = useContext(ThemeContext) if (value === null) throw new Error("useTheme must be used inside <ThemeProvider>") return value} export function ThemeButton() { const { theme, toggle } = useTheme() return <button onClick={toggle}>Theme: {theme}</button>}Since React 19 the context itself is the provider (<ThemeContext value={...}>); older code writes <ThemeContext.Provider value={...}>, which still works. useContext finds the nearest provider above the calling component; with none, it returns the default given to createContext. React 19's use(ThemeContext) reads context too, and unlike a hook it may be called inside a condition.
Who re-renders
When ThemeProvider renders with a value that is not Object.is-equal to the previous one, React finds every component below it that reads ThemeContext and renders it, even if it is wrapped in memo and its props are unchanged. Components in between that don't read the context are not rendered because of it.
Two consequences:
- The value's identity matters. Without the
useMemoabove,valuewould be a new object every timeThemeProviderrenders, and every reader would render with it, even whenthemehasn't changed. The React Compiler memoises this for you. childrenare not re-rendered. Becausechildrenwere created byThemeProvider's parent, athemechange re-renders the provider and the context readers, not the whole tree it wraps.
Splitting and choosing
- Put values that change at different rates in different contexts. A
userthat changes on login and acursorPositionthat changes 60 times a second should not share a provider, or every user-reader renders at 60 frames per second. - Separate the state from the functions that change it (
TasksContextandTasksDispatchContext). Components that only dispatch then never render because of the data. - Context is a way to pass values, not a state manager. For large, frequently updated shared state where each component needs only a slice, a store with selectors (for example through
useSyncExternalStore) re-renders only the components whose slice changed. - Before reaching for context, try passing props or composing with
children. A few levels of props are explicit and easy to follow.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.