10/25 lessons40%

Performance Pitfalls of Context (and How to Dodge Them)

Using React Context feels like magic… until your app starts lagging and you have no idea why. I’ve been there—thinking I was simplifying my state management, only to end up with components re-rendering like it’s a Black Friday sale. If your app feels slower after adding context, you’re not imagining it. Context can wreck performance if you don’t know how to use it right. Let’s fix that.

Why Context Re-renders Everything (Even the Stuff That Didn’t Change)

Here’s the brutal truth: `useContext()` doesn’t care what part of the value changed—it just re-renders everything consuming that context whenever the value object changes.

I once had a form where I used a single FormContext for tracking validation state, field errors, touched inputs, and theme styles. It was "convenient," until I noticed every field component was re-rendering on every keystroke—even untouched ones.

The culprit? This:

javascript
1
          <FormContext.Provider value={{ errors, touched, theme }}>
        

React sees a brand-new object on every render, even if only errors changed. That’s a rerender party you didn’t ask for.

If you’ve ever watched your DevTools Profiler turn into a Christmas tree after a tiny state update, you’ve been burned by this.

Split Providers Like a Pro (Yes, Even if It Gets Nest-y)

This is where I used to resist. “I don’t want five nested providers—it looks messy.” But if you want performance, split your context by concern.

Bad:

javascript
1
          <AppContext.Provider value={{ user, theme, modal }}>
        

Better:

javascript
1
2
3
4
5
6
7
          <UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <ModalContext.Provider value={modal}>
      {children}
    </ModalContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>
        

Yeah, it’s a nesting doll. But you gain granular updates. A modal state change won’t make your user avatar rerender anymore. That’s a trade I’ll take any day.

Want cleaner code? Just abstract the nesting into a single AppProviders component. Ugly JSX shouldn't be your excuse for poor performance.

Memoizing Context Consumers (This Isn’t Optional)

If your consumers are doing too much work, wrap them with `React.memo()`. It doesn’t magically fix everything, but it helps a lot—especially if the component also receives props.

Example:

javascript
1
2
3
4
          const Avatar = React.memo(function Avatar({ size }) {
  const user = useContext(UserContext);
  return <img src={user.avatar} width={size} />;
});
        

The real kicker? Memoize the value you provide in the first place:

javascript
1
2
          const themeValue = useMemo(() => ({ color, darkMode }), [color, darkMode]);
<ThemeContext.Provider value={themeValue}>
        

Without this, even stable state causes rerenders because the reference changes. I ignored this early on because I thought useMemo was “premature optimization.” I was wrong. If your value is an object, memoize it. Always.

Render Batching and Selective Updates (A Little Goes a Long Way)

React usually batches state updates. But when you’re updating multiple state slices that feed into a single context, be careful.

Here’s a problem I hit:

javascript
1
2
          setUser(newUser);
setPermissions(newPermissions);
        

If both user and permissions live inside a single context value, this could cause two separate rerenders. Yikes.

Solution? Group them:

javascript
1
2
3
4
5
          setAuthState(prev => ({
  ...prev,
  user: newUser,
  permissions: newPermissions
}));
        

And while you’re at it—do not over-centralize fast-changing state in context. Typing, animations, scroll position—these don’t belong in shared context. They’ll make your entire tree jump like a nervous squirrel.

Split that state out into local components or isolate them using libraries like Zustand, or just pass props down. Not every piece of state needs to be “shared.”

Refactoring to Isolate Expensive State (Or: Don't Be Lazy)

This one’s about discipline. Back in a dashboard app I worked on, I had context values for everything from the logged-in user to the current search input. Guess which one changed constantly? The search input. Guess what caused a 400ms UI delay? Everything else re-rendering with it.

Here’s what I do now: I move fast-changing or deeply nested state into their own components and avoid pushing it into context at all.

Example:

javascript
1
2
3
4
5
6
7
8
9
10
11
          function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const move = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", move);
    return () => window.removeEventListener("mousemove", move);
  }, []);

  return <MouseContext.Provider value={position}>{children}</MouseContext.Provider>;
}
        

Now your mouse position updates won’t wreck the rest of your app. React stays happy. Your frame rate stays above 30. Your boss stops asking, “Why is the app slow?”

Don't be lazy with context. Just because you can add something there doesn’t mean you should.

Up next, we’ll explore how to make Context smart—and snappy—using selectors and other advanced patterns. **UseContextSelector and Other Smart Patterns** will show you how to subscribe to only the bits of context you care about—no more, no less. It’s like Redux selectors... but cooler.