Skip to content

Fix: React Context Not Updating / Re-rendering Components

FixDevs ·

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

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

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 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 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 useCallback as well. Functions are recreated on every render by default, so useMemo on 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.

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