Fix: React Context Not Updating / Re-rendering Components
Quick Answer
How to fix React Context not triggering re-renders — caused by mutating state directly, wrong provider placement, stale closures, and missing value changes that React can detect.
The Error
You update a value in React Context, but the consuming components do not re-render. Or they re-render, but they still show the old value. There is no error — the UI just does not update.
Common symptoms:
useContext(MyContext)returns the initial value even after the provider’s state changes.- A component re-renders but
context.valueis stale. - Context updates in one component but another component that consumes the same context does not reflect the change.
- The provider’s state changes correctly (you can log it), but consumers do not update.
- Context works in development but not after a production build.
Why This Happens
React Context triggers a re-render in all consuming components whenever the value prop passed to the Provider changes. The problems occur when:
- State is mutated directly instead of replaced — React sees the same object reference and skips re-rendering.
- The consumer is not inside the Provider — a component consuming context that is above its Provider gets the default value, not the Provider’s value.
- The
valueprop is a new object on every render — causes unnecessary re-renders of all consumers (performance issue, not a “no update” issue). - Stale closure — a function in context captures an old value and never reads the updated one.
- Multiple Provider instances — the consuming component subscribes to a different Provider than the one being updated.
- Using the wrong context object — importing context from a different file or re-creating it.
Fix 1: Never Mutate State Directly
React uses referential equality to detect context changes. If you mutate an object or array in place, the reference stays the same and React skips the update.
Broken — mutating the array directly:
const [items, setItems] = useState([]);
const addItem = (newItem) => {
items.push(newItem); // Mutates in place — same array reference
setItems(items); // React sees the same reference — no re-render
};Fixed — create a new array:
const addItem = (newItem) => {
setItems([...items, newItem]); // New array reference — triggers re-render
};Broken — mutating an object:
const [user, setUser] = useState({ name: "Alice", role: "user" });
const updateRole = (newRole) => {
user.role = newRole; // Mutates in place
setUser(user); // Same reference — no update
};Fixed — spread into a new object:
const updateRole = (newRole) => {
setUser({ ...user, role: newRole }); // New object reference
};Why this matters: React’s
useStateanduseReduceruseObject.is()to compare old and new values.Object.is(arr, arr)is alwaystrue— the same reference always equals itself. You must create a new value to trigger a re-render.
Fix 2: Verify the Consumer Is Inside the Provider
A component consuming context must be rendered inside the matching Provider in the component tree. If it is above the Provider or in a different tree branch, it gets the default context value (the one passed to createContext()).
Broken — consumer above provider:
function App() {
return (
<div>
<Navbar /> {/* Consumes ThemeContext — gets default value */}
<ThemeProvider>
<Main />
</ThemeProvider>
</div>
);
}Fixed — wrap everything in the provider:
function App() {
return (
<ThemeProvider>
<Navbar /> {/* Now inside ThemeProvider — gets the correct value */}
<Main />
</ThemeProvider>
);
}Diagnose with React DevTools: Open the Components panel, find the consuming component, and look at its “Context” section. It shows which context it is subscribed to and the current value. If it shows the default value (undefined, null, or whatever you passed to createContext()), the component is not inside the Provider.
Fix 3: Ensure You Are Using the Same Context Object
Context identity matters. If you import the context from two different files, or accidentally create it twice, consumers and providers are talking to different objects.
Broken — context created in two places:
// context/theme.js
export const ThemeContext = createContext("light");
// components/ThemeProvider.jsx
import { createContext } from "react"; // Wrong — creates a second context!
const ThemeContext = createContext("light");Fixed — always import from one source:
// context/theme.js — single source of truth
export const ThemeContext = createContext("light");
// components/ThemeProvider.jsx
import { ThemeContext } from "../context/theme";
// components/Navbar.jsx
import { ThemeContext } from "../context/theme";Both the Provider and all consumers must import ThemeContext from the same file.
Fix 4: Fix Stale Closures in Context Functions
When you put functions into context, they can capture stale state values via closures:
Broken — stale closure:
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (newItem) => {
// `items` is captured from the render when addItem was created
// If items has changed since, this is stale
setItems([...items, newItem]);
};
return (
<CartContext.Provider value={{ items, addItem }}>
{children}
</CartContext.Provider>
);
}When addItem is called after multiple state updates, it may use an outdated items value.
Fixed — use the functional updater form:
const addItem = (newItem) => {
setItems(prevItems => [...prevItems, newItem]); // Always uses the latest state
};The functional updater setItems(prev => ...) always receives the current state value, bypassing the stale closure problem. Use this pattern for all state updates inside context functions.
Fix 5: Fix the Provider Value Causing Unnecessary Re-renders
If you create a new object for value on every render, all consumers re-render every time the Provider re-renders — even if the data has not changed:
Broken — new object on every render:
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
// This creates a new object on every render of UserProvider
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}Fixed — memoize the value:
import { useMemo } from "react";
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}With useMemo, the value object is only recreated when user changes. Consumers only re-render when user actually changes.
Pro Tip: For functions in context, wrap them in
useCallbackas well. Functions are recreated on every render by default, souseMemoon the value object won’t help if the functions inside change:const addItem = useCallback((newItem) => { setItems(prev => [...prev, newItem]); }, []); // No dependencies — setItems is stable
Fix 6: Split Context to Avoid Over-rendering
If a context holds many values, every consumer re-renders when any value changes — even values that consumer does not use. Split large contexts into smaller, focused ones:
Broken — one large context:
// Every component using this context re-renders when user OR theme OR cart changes
const AppContext = createContext({ user, theme, cart, setUser, setTheme, setCart });Fixed — separate contexts:
const UserContext = createContext({ user, setUser });
const ThemeContext = createContext({ theme, setTheme });
const CartContext = createContext({ cart, setCart });A component that only needs theme subscribes to ThemeContext only and does not re-render when user or cart changes.
Alternatively, use useContextSelector (from the use-context-selector package) to subscribe to a specific part of a context:
import { useContextSelector } from "use-context-selector";
// Only re-renders when user.name changes, not the entire user object
const userName = useContextSelector(UserContext, ctx => ctx.user.name);Fix 7: Debug Context with a Custom Hook
Wrap useContext in a custom hook to add validation and debugging:
import { useContext } from "react";
import { CartContext } from "./CartContext";
export function useCart() {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error("useCart must be used inside a CartProvider");
}
return context;
}This gives you a clear error message if a component uses the hook outside the Provider, instead of silently getting the default value. Use this pattern for every context you create.
Add logging to trace updates:
function CartProvider({ children }) {
const [items, setItems] = useState([]);
useEffect(() => {
console.log("Cart updated:", items);
}, [items]);
// ...
}Still Not Working?
Check for React version issues. Context re-rendering behavior has changed across versions. React 18 introduced automatic batching — multiple state updates in a single event handler are batched into one re-render. This is usually good, but if you expect intermediate states to trigger renders, they might be batched. Use flushSync if you need synchronous rendering.
Check for Strict Mode double-invocation. In React 18 with <React.StrictMode>, components render twice in development to detect side effects. This does not cause issues with context, but can make logging confusing — you will see effects run twice.
Check that you are not using the context before the Provider mounts. If a component mounts before its Provider (e.g., due to conditional rendering), it gets the default context value. The fix is to ensure the Provider always renders before consumers.
Consider Zustand, Jotai, or Redux for complex state. Context is not a state management library — it is a dependency injection mechanism. For complex, frequently-updating global state, a dedicated state library is more efficient and easier to debug. Context re-renders all consumers on every update; Zustand and Jotai use subscriptions to re-render only components that use the specific state that changed.
For related React rendering issues, see Fix: React Too Many Re-renders and Fix: React useEffect Infinite Loop.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: No Routes Matched Location in React Router v6
How to fix 'No routes matched location' in React Router v6 — caused by missing Routes wrapper, wrong path syntax, nested route mismatches, and v5 to v6 migration issues.
Fix: CSS Custom Properties (Variables) Not Working or Not Updating
How to fix CSS custom properties not applying — wrong scope, missing fallback values, JavaScript not setting variables on the right element, and how CSS variables interact with media queries and Shadow DOM.
Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.
Fix: React Query (TanStack Query) Infinite Refetching Loop
How to fix React Query refetching infinitely — why useQuery keeps fetching, how object and array dependencies cause loops, how to stabilize queryKey, and configure refetch behavior correctly.