Skip to content

Fix: React Context Not Updating / Re-rendering Components

FixDevs · (Updated: )

Part of:  React & Frontend Errors

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 Silent Re-Render Failure

Personally, I think React Context is one of the most misdiagnosed APIs because there is no error message when it stops working: components just silently render stale values. The cause is almost always one of three things: state mutation, wrong Provider placement, or two different Context objects. I learned to open React DevTools’ Components panel before changing any code. 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.value is 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.

Quick Reference Before You Dive In

If you arrived here from Google with a fresh non-updating context, the five facts that resolve roughly 90 percent of cases:

  1. React Context uses REFERENTIAL EQUALITY (Object.is) to detect changes. Mutating an array or object in place leaves the reference identical and React skips the update. Always create new objects / arrays. The React Context documentation and the useContext reference are the canonical sources.
  2. Open React DevTools’ Components panel FIRST. It shows which Context object each consumer is subscribed to and the current value. If the value is the default, the consumer is not inside the Provider.
  3. The Provider must wrap the consumer in the component tree. A consumer above the Provider gets the default value passed to createContext(), not the Provider’s value.
  4. Both Provider and consumers MUST import from the SAME file. Re-importing createContext in different files creates separate Context objects that do not share values.
  5. Use the FUNCTIONAL updater form (setItems(prev => ...)) in context functions. Direct closure over items can capture a stale value.

The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.

Why Context Silently Stops Working

React Context triggers a re-render in all consuming components whenever the value prop passed to the Provider changes. “Changes” is determined by Object.is(); React performs a reference-equality check between the previous and current value. If the reference is the same, React assumes nothing changed and skips re-rendering, even if you mutated nested fields. This is intentional: it lets React skip work when nothing meaningful changed, and it forces you to be explicit about updates by replacing the value rather than editing it in place.

The mismatch between “I mutated something” and “React sees a change” is the root of the most common bug. Pushing into an array, assigning to an object property, or modifying a nested object all leave the outer reference identical. Calling setState(theSameObject) does nothing because React’s bail-out check sees no change. To trigger an update you must create a new object ({ ...prev } for objects, [...prev] for arrays) or use Immer’s draft-based mutations which produce a new reference under the hood.

A second category of cause is structural: the consumer is not actually subscribed to the Provider you think it is. Either it sits above the Provider in the tree (and sees only the default value from createContext), or it imports a different Context object than the Provider does. Module-bundling oddities (duplicate copies of a library, two contexts created in different files, hot-reloading drifting context identity) all manifest as “the value never updates.” React DevTools’ Components panel reveals which Context object a consumer is subscribed to and what value it sees; that is the fastest way to confirm whether the wiring is correct.

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 value prop 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.

Platform and Environment Differences

Context behavior is identical at the React core level across environments, but the framework wrapping React and the JavaScript runtime introduce significant differences in when and how context flows.

Next.js App Router: server vs client components. Context only works inside client components (files marked with "use client"). Server components run on the server and produce HTML; they have no rerender lifecycle, so context is meaningless there. A common failure mode is rendering <ThemeProvider> (which is a client component) inside a server component, then trying to read context in a sibling server component. The sibling cannot read context because it never executes on the client. The fix is either to push the Provider to the root layout’s client boundary or to thread the value as props down through server components.

Next.js Pages Router (and other SSR frameworks). With Pages Router, the entire tree renders on the server during getServerSideProps/getStaticProps and the client hydrates. Context works during SSR as long as the Provider is in _app.tsx. If your Provider initializes state from window or document, that state is undefined on the server, then changes on the client, causing hydration mismatches. See Fix: Next.js Hydration Failed.

React Native: platform-specific re-renders. React Native uses Fabric (the new renderer) on most current builds, which schedules updates via the platform’s main thread. Context updates propagate through the same React reconciliation, so the rules are identical to web. The difference is timing: on iOS, view updates are bound to CADisplayLink (60Hz or 120Hz); on Android, the Choreographer batches them. A context update fired in a useEffect may visually settle one frame later than expected. This rarely matters for correctness but can produce confusing logs when comparing platforms.

React Native Web vs React Native. A shared codebase using react-native-web produces two slightly different runtime behaviors. On web, microtasks flush immediately after the macrotask, so a context update inside Promise.resolve().then(...) is visible synchronously to the next render. On native, the JavaScript runtime (Hermes or JSC) batches microtasks slightly differently and updates can appear one tick later. Use React.startTransition to make these consistent.

Electron renderer process. Each BrowserWindow is a separate renderer process with its own React tree. Contexts do not cross process boundaries; a Provider in window A is not visible to consumers in window B. To share state across windows, use IPC (ipcMain / ipcRenderer) or a shared store backed by electron-store. The renderer also runs in a slightly modified V8 environment where some browser APIs behave differently, but React context itself works the same.

Web Workers and Service Workers: context isolation. A Web Worker is a separate global scope. React running inside a Worker (e.g., via React Server Components or partytown) has its own context tree completely independent of the main window’s. There is no built-in mechanism to share a React Context across the postMessage boundary; you have to thread values manually. This is rarely an issue because React in a Worker is unusual.

Microtask vs macrotask scheduling per runtime. React 18+ batches state updates inside event handlers and microtasks. A setState followed by another setState in the same handler results in one re-render. Across runtimes, the microtask queue is implementation-specific; Chrome, Safari, Firefox, Node, Bun, and Deno all process microtasks slightly differently. The visible behavior is the same (the second-to-last setState wins), but timing-sensitive tests that count renders may see different counts on different runtimes.

React.StrictMode double-rendering in development. Under <React.StrictMode> in React 18+, components mount, unmount, and re-mount during development. Providers run their initialization logic twice. If your Provider has side effects in render (which it should not, but sometimes does: opening a WebSocket, starting a timer), you get double the side effects in dev and exactly one in production. This catches violations of the rules of hooks. See Fix: React useEffect Runs Twice.

When to Use Which Fix

The next seven sections cover the fixes in detail. The table below maps your situation to the recommended fix.

Your situationRecommended fixWhy
State mutated in place (push, assignment)Fix 1: spread into new object / arrayReferential equality
Consumer renders default value (DevTools)Fix 2: move Provider above consumerTree position matters
Two Context imports from different filesFix 3: import from single sourceContext identity
Function in context uses stale stateFix 4: functional updater setX(prev => ...)Avoids closure capture
All consumers re-render too oftenFix 5: memoize value with useMemoNew value object every render
Large context, partial useFix 6: split into smaller contextsPer-context subscription
Need diagnostics and validationFix 7: custom hook wrapper with throwClear error if outside Provider

If multiple rows apply, pick the topmost one.

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
};

The reason this matters: React’s useState and useReducer use Object.is() to compare old and new values. Object.is(arr, arr) is always true; the same reference always equals itself. You must create a NEW value (not mutate the existing one) for React to detect the change. This rule applies everywhere in React; I think of it as the single most important rule of the framework.

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.

A subtlety I have walked into more than once: useMemo on the value object only stabilizes the WRAPPER. If the value includes a function defined inline on every render, the function itself is a new reference even though the wrapper memo says “same value.” Wrap functions in useCallback separately and reference them in the memoized value:

const addItem = useCallback((newItem) => {
  setItems(prev => [...prev, newItem]);
}, []); // No dependencies; setItems is stable
const value = useMemo(() => ({ items, addItem }), [items, addItem]);

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]);

  // ...
}

Stranger Causes I Have Tracked Down

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.

Check for duplicate React copies in node_modules. If your bundler ends up with two copies of React (common when a library declares React as a regular dependency instead of a peer dependency), every createContext call from the duplicate library creates a context object the rest of the app cannot see. Run npm ls react and confirm only one version is installed. Yarn and pnpm offer resolutions and overrides to force deduplication.

Check for useState initializer functions running every render. A common pattern is useState(() => createDefault()). The initializer runs only on first render. But useState(createDefault()) runs every render and discards the result; your state never updates because the initial value is “recomputed” but the state is preserved. This is not a context problem strictly but produces the same observed symptom of “value never updates.”

Check for context inside React.memo boundaries. React.memo skips re-rendering when props are shallow-equal. If a memoized component consumes context, it still re-renders when context changes; memo does not block context updates. But if the memoized component then calls useContext and the context value is referentially stable (via useMemo), the memoized inner tree may not re-render visibly. This is correct behavior but counterintuitive when debugging.

Check React Compiler optimizations (React 19+). The new React Compiler auto-memoizes components and values. In some cases it can cache context-derived values in a way that makes them appear stale during HMR. Disable the compiler for the affected file ("use no memo" directive) to isolate whether the compiler is the cause.

What Other Tutorials Get Wrong About React Context

Most React tutorials list the same fixes but frame them in ways that produce subtle bugs.

They miss the referential-equality rule. Articles that show setItems(items) after a push() train readers on a pattern that silently fails. The rule is universal: React detects changes via Object.is; mutation never triggers an update.

They show value={{ user, setUser }} without useMemo. Every render creates a new object, every consumer re-renders unnecessarily. Articles that omit useMemo produce subtle performance bugs that look fine in dev and degrade under load.

They miss the stale-closure trap. Functions in context that close over items use the value from THAT render. The functional updater setX(prev => ...) avoids it. Articles that show setItems([...items, newItem]) miss this trap.

They miss the “import from the same file” rule. Two createContext calls in different files create two different Context objects. Provider and consumer must import from the SAME source. Articles that show createContext examples without flagging this leave readers debugging silent miswiring.

They confuse Context with state management. Context is a dependency injection mechanism, not a Redux replacement. For frequently-updating global state with many consumers, Zustand / Jotai are more efficient. Articles that recommend Context for everything produce performance issues at scale.

They miss Next.js App Router server / client distinction. Context only works in client components. Articles that show Context examples without flagging the "use client" requirement produce confusion in App Router projects.

Frequently Asked Questions

Why does my context not update when I push to an array?

React detects context changes via reference equality (Object.is). items.push(x) mutates the array in place; the reference is identical. React skips the re-render. Use setItems([...items, newItem]) to create a NEW array reference, which React detects as a change.

Should I use Context for global state?

For occasional, semantically-meaningful global state (theme, locale, user identity), yes. For frequently-updating global state with many consumers (cart items in an e-commerce app, real-time data), prefer a dedicated state library (Zustand, Jotai, Redux). Context re-renders ALL consumers on every update; libraries with selector subscriptions re-render only the consumers that use the specific value that changed.

Why does my consumer get undefined instead of the provider’s value?

The consumer is rendered above the Provider in the component tree, OR the consumer imports a different Context object than the Provider. Use React DevTools’ Components panel to confirm which Context the consumer is subscribed to and whether it sees the default value or the Provider’s value.

Does React.memo prevent context updates?

No. React.memo skips re-renders when PROPS are shallow-equal, but context updates always trigger re-renders of consumers regardless of props. If you want to skip context-driven re-renders for parts of a tree, use use-context-selector to subscribe to specific slices of context.

Why does my Next.js App Router consumer not see the Provider’s value?

Server components cannot read context; they have no client-side rerender lifecycle. Wrap the Provider in a client component (with "use client") and ensure the consumer is also a client component, OR thread the value as props through server components.

Should every consumer be wrapped in a custom hook?

Yes for most cases. A custom hook like useCart() that throws when used outside the Provider gives you a clear error instead of silently returning the default value. The pattern is simple, defensible, and produces better debugging output across the codebase.

For related React rendering issues, see Fix: React Too Many Re-renders and Fix: React useEffect Infinite Loop.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles