Kodokon kodokon.com

Context: sharing without prop drilling

Move data around without prop drilling using context, while keeping its re-renders and limits under control.

8 min · 3 questions

Open this lesson in Kodokon

Prop drilling refers to those props that pass through three or four intermediate components without being used there, just to reach a leaf of the tree. Every layer they cross becomes coupled to data it does not care about. Context solves this transport problem: createContext creates the channel, a provider publishes a value, and useContext reads it from any descendant. Keep the nuance in mind: context transports state, it does not manage it - the value still lives in a component, here the provider.

JSX
import { createContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
The published value is memoized to keep its reference stable.

When the provider's value changes, all consumers re-render, even deeply nested ones - and React.memo does not protect them, because it compares props, not context. The classic trap makes things worse: writing value={{ theme, setTheme }} creates a fresh object on every render of the provider, hence a new reference, hence cascading re-renders even when theme has not moved. That is what the useMemo above is for. Round out the setup with an access hook that fails loudly outside the provider.

JSX
import { useContext } from "react";

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error(
      "useTheme must be inside <ThemeProvider>"
    );
  }
  return context;
}
An explicit error beats a silent null.

Before creating a context, exhaust the composition option first. If an intermediate layer does nothing but relay a prop, invert the structure: build the element at the top of the tree, where the data lives, and pass it down via children or a dedicated prop. The intermediate layer becomes generic and the data no longer passes through any component that does not use it.

JSX
function App({ currentUser }) {
  return (
    <Layout
      header={<TopBar user={currentUser} />}
      content={<Dashboard />}
    />
  );
}
Layout knows nothing about currentUser: it slots in ready-made elements.

Knowledge check

Make sure you remember the key points of this lesson.

  1. A context consumer is wrapped in React.memo and the context value changes. What happens?
    • It does not re-render: React.memo blocks all updates
    • It re-renders: React.memo compares props, not context values
    • It re-renders only if its props change too
  2. Why memoize the object passed to value in a provider?
    • To speed up the creation of the object on each render
    • Because the provider requires an immutable value
    • So the reference only changes when the data changes, avoiding unnecessary re-renders of the consumers
  3. What is the main limitation of context for very frequently updated state?
    • No selectors: every update re-renders all consumers, even those that only use part of the value
    • Context is asynchronous and delays updates
    • Context can only store primitive values