React Context can cause unexpected performance issues because any component that consumes a context value will re-render whenever that context value changes, even if the specific part of the value the component uses hasn’t changed.

Let’s see it in action. Imagine a simple UserContext holding user data and a ThemeContext holding the current theme.

// App.js
import React, { useState, createContext, useContext } from 'react';

const UserContext = createContext({ user: null });
const ThemeContext = createContext({ theme: 'light' });

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Alice' });
  return (

    <UserContext.Provider value={{ user, setUser }}>

      {children}
    </UserContext.Provider>
  );
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (

    <ThemeContext.Provider value={{ theme, setTheme }}>

      {children}
    </ThemeContext.Provider>
  );
}

function UserDisplay() {
  const { user } = useContext(UserContext);
  console.log('UserDisplay re-rendered');
  return <div>User: {user.name}</div>;
}

function ThemeDisplay() {
  const { theme } = useContext(ThemeContext);
  console.log('ThemeDisplay re-rendered');
  return <div>Theme: {theme}</div>;
}

function App() {
  const { setUser } = useContext(UserContext); // Consuming to demonstrate re-render
  const { setTheme } = useContext(ThemeContext); // Consuming to demonstrate re-render

  return (
    <div>
      <button onClick={() => setUser({ name: 'Bob' })}>Change User</button>
      <button onClick={() => setTheme('dark')}>Change Theme</button>
      <UserDisplay />
      <ThemeDisplay />
    </div>
  );
}

function Root() {
  return (
    <UserProvider>
      <ThemeProvider>
        <App />
      </ThemeProvider>
    </UserProvider>
  );
}

export default Root;

If you click "Change Theme," ThemeDisplay correctly re-renders. But UserDisplay also re-renders, even though its data hasn’t changed. This happens because UserContext.Provider’s value prop (which is { user, setUser }) is technically a new object on every render of App, causing UserContext consumers to update. The same issue occurs if you change the user and ThemeDisplay re-renders unnecessarily.

The core problem is that React’s default useContext hook triggers a re-render in all consuming components whenever the entire context value object changes. This is often fine for small applications, but in larger ones, or with frequently changing context values, it leads to a cascade of unnecessary work.

The mental model here is that context acts like a global bus. When you send a message on the bus, everyone listening hears it. If you have multiple independent signals on the same bus, everyone hears all signals, even the ones they don’t care about.

The solution is to split your context into smaller, more specific contexts. Instead of one large UserAndThemeContext, you have UserContext and ThemeContext.

// App.js (Revised)
import React, { useState, createContext, useContext, useMemo } from 'react';

const UserContext = createContext({ user: null });
const ThemeContext = createContext({ theme: 'light' });

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Alice' });
  // Memoize the value to prevent unnecessary re-renders when theme changes
  const value = useMemo(() => ({ user, setUser }), [user]);
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  // Memoize the value to prevent unnecessary re-renders when user changes
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function UserDisplay() {
  const { user } = useContext(UserContext);
  console.log('UserDisplay re-rendered');
  return <div>User: {user.name}</div>;
}

function ThemeDisplay() {
  const { theme } = useContext(ThemeContext);
  console.log('ThemeDisplay re-rendered');
  return <div>Theme: {theme}</div>;
}

// Components that only need one context
function UserProfile() {
  const { user } = useContext(UserContext);
  console.log('UserProfile re-rendered');
  return <div>Profile for {user.name}</div>;
}

function ThemeSwitcher() {
  const { theme, setTheme } = useContext(ThemeContext);
  console.log('ThemeSwitcher re-rendered');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Switch to {theme === 'light' ? 'Dark' : 'Light'}
    </button>
  );
}


function App() {
  // App component itself doesn't need to consume both contexts directly
  // if its children handle their own context consumption.
  return (
    <div>
      <UserProfile />
      <ThemeSwitcher />
      <UserDisplay /> {/* Still here to show context usage */}
      <ThemeDisplay /> {/* Still here to show context usage */}
    </div>
  );
}

function Root() {
  return (
    <UserProvider>
      <ThemeProvider>
        <App />
      </ThemeProvider>
    </UserProvider>
  );
}

export default Root;

In this revised version, we’ve introduced useMemo within the provider components. UserProvider now memoizes its value based on user, and ThemeProvider memoizes its value based on theme. This means the value object itself only changes when the specific state it contains changes.

When you click "Change Theme," ThemeContext.Provider’s value changes. ThemeDisplay and ThemeSwitcher re-render. However, UserContext.Provider’s value (which is memoized and depends only on user) does not change. Therefore, UserDisplay and UserProfile do not re-render. This is the key: separating concerns and memoizing provider values prevents unrelated components from being dragged into re-renders.

The most surprising true thing about React Context is that it doesn’t inherently track which part of the context value a component is subscribed to; it only knows if the context value object identity has changed since the last render. This is why even if you only read user.name from UserContext and the setUser function within the context value changes, the consuming component will re-render because the value object passed to UserContext.Provider is a new instance.

If you have a very large context object with many independent pieces of state, you might even consider creating separate context providers for each distinct piece of state. For example, instead of a single UserProvider managing user and settings, you’d have UserProvider and UserSettingsProvider, each with their own Context.Provider and useContext hook. This granular approach ensures that only components dependent on a specific piece of state will re-render when that state changes.

The next concept you’ll likely encounter is managing complex state within contexts, leading into patterns like state reducers or libraries like Zustand or Jotai for more advanced state management.

Want structured learning?

Take the full React course →