Context has exactly one update rule: when a provider's value differs from its previous value under Object.is, every component below it that reads that context re-renders. Two consequences trip people up. The first is identity: an object or array written inline in JSX is rebuilt on every render of the providing component, so the value always "differs" even when the data is identical. The second is that memo (memo) does not help — a memoized component whose props are unchanged still re-renders if it reads a context whose value changed. Memoization guards props, not context, and the React 7,897 Compiler (The React Compiler) does not change that.
const AuthContext = createContext(null); // changes when the user changes
const AuthActions = createContext(null); // never changes after mount
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const actions = useMemo(() => ({
login: (u) => setUser(u), logout: () => setUser(null)
}), []); // no dependencies: stable forever
return <AuthContext value={user}>
<AuthActions value={actions}>{children}</AuthActions>
</AuthContext>;
}A login button reads AuthActions only, so it never re-renders when the user changes. Note also that AuthProvider receives its subtree through children: that JSX is created by the parent, so it is the same element object on every render of AuthProvider, and React skips re-rendering it when only the provider's state changed.
| Technique | What it fixes | Cost |
|---|---|---|
| useMemo on the value | A new object identity on every parent render | A dependency array to maintain |
| Split data and actions | Action-only readers re-render on data changes | Two providers, two hooks |
| Provider takes children | The whole app re-renders with the provider | One extra component |
| use-context-selector | Readers re-render on fields they ignore | A dependency; last release 2024 |