Chapter 3 of 4

Context and Re-renders

The one performance trap everyone hits, and how to avoid it.

Every component that reads a context re-renders when the provider's value changes - and React compares that value by reference.

The classic mistake

Every render of App re-renders every consumer, even if user never changed.
function App() {
  const [user, setUser] = useState(null);

  // A brand new object on every render of App
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Everything />
    </UserContext.Provider>
  );
}

The fix is to memoise the object so its identity is stable while its contents are.

function App() {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({ user, setUser }), [user]);

  return (
    <UserContext.Provider value={value}>
      <Everything />
    </UserContext.Provider>
  );
}

Split contexts that change at different rates

If one part of the value changes constantly and another rarely, put them in separate contexts. Components that only need the stable half stop re-rendering.

// Changes on every keystroke
<SearchQueryContext.Provider value={query}>
  {/* Changes almost never */}
  <SearchActionsContext.Provider value={actions}>
    <Results />
  </SearchActionsContext.Provider>
</SearchQueryContext.Provider>

Keep the provider high and the tree below it stable

Children passed to a provider as children are created by the parent, so they do not re-render just because the provider's value changed - only the consumers do.

Memoise a context value by hand

Complete makeValueCache so it returns the same object while the user id is unchanged, and a new object when it changes.