React & Next.js

Context — passing data without prop drilling

Context doesn't replace props, and it doesn't replace one-way data flow — it's a way to skip the intermediate hops for a value that genuinely needs to reach many components at many depths, without turning every layer in between into a pass-through.

Intermediate

4 min read

Creating and providing a context

import { createContext, useContext } from "react";
 
const ThemeContext = createContext("light"); // "light" is the default, used if no Provider wraps a component
 
function App() {
  const [theme, setTheme] = useState("dark");
  return (
    <ThemeContext.Provider value={theme}>
      <Everything /> {/* every descendant, at any depth, can now read theme */}
    </ThemeContext.Provider>
  );
}

createContext(defaultValue) creates a context object — a channel a value can travel through outside the normal prop chain. <ThemeContext.Provider value={theme}> makes theme available to every component rendered anywhere inside it, at any depth, without passing it as an explicit prop through each intermediate layer. The defaultValue passed to createContext is only used if a component reads the context with no Provider above it in the tree at all — in real apps, there's almost always a Provider somewhere, so the default mostly matters for tests or genuinely standalone usage.

Reading a context value: useContext

function DeepChild() {
  const theme = useContext(ThemeContext); // reads whatever the nearest Provider above it set
  return <div className={theme}>...</div>;
}

Any component anywhere inside the Provider can call useContext(ThemeContext) to read the current value directly — no props were passed to DeepChild at all, and no intermediate component between App and DeepChild needed to know theme even existed. This is the direct fix for the prop-drilling problem from the previous lesson: components that don't care about a value no longer have to forward it, and components that do care can reach in and grab it directly, regardless of how many layers deep they actually are.

Why context is not a general-purpose state-management replacement

// A genuinely bad use of context — this is just avoiding props for no real reason
function UserAvatar() {
  const user = useContext(UserContext);
  return <img src={user.avatarUrl} />;
}
// vs. the plain-props version, which is simpler and just as effective here:
function UserAvatar({ avatarUrl }) {
  return <img src={avatarUrl} />;
}

Context solves one specific problem: avoiding drilling a value through many uninterested intermediate layers. It does not replace the case where a value is only needed by a component's direct children — plain props are simpler, more explicit, and easier to trace there, and reaching for context reflexively for every piece of shared data (rather than only once prop drilling is a genuine, measured pain point) trades that explicitness away for very little real benefit. Context is the right tool specifically once several components at meaningfully different depths need the same value, and the intermediate components genuinely have nothing to do with it.

A real performance trap: every consumer re-renders on any value change

function AppState() {
  const [user, setUser] = useState({ name: "Ada" });
  const [theme, setTheme] = useState("dark");
 
  // Bundling unrelated values into one context object —
  // ANY change to either one re-renders every component reading this context
  return (
    <AppContext.Provider value={{ user, theme, setUser, setTheme }}>
      <Everything />
    </AppContext.Provider>
  );
}

Every component calling useContext on a given context re-renders whenever that context's value changes — including components that only actually care about theme, when it was really user that changed. Bundling several unrelated pieces of state into one context object (as shown here) means a change to any of them re-renders every consumer of the whole context, regardless of which specific piece they actually read. The fix, when this becomes a real measured problem, is usually splitting into multiple, more narrowly-scoped contexts (a UserContext and a separate ThemeContext) so a component only re-renders for the specific slice of data it actually subscribes to.

The typical real-world shape: a context plus a custom hook, paired together

const AuthContext = createContext(null);
 
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>;
}
 
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error("useAuth must be used inside an AuthProvider");
  return context;
}
 
// Usage anywhere in the tree, with a clear error if it's ever used incorrectly
function Header() {
  const { user } = useAuth();
  return <p>{user ? `Hi, ${user.name}` : "Not logged in"}</p>;
}

Wrapping useContext(AuthContext) in a small custom hook (useAuth, covered properly in the next lesson) is the standard, idiomatic pattern in real React codebases — it gives consumers a clean, purpose-named API (useAuth() instead of useContext(AuthContext) everywhere), and it's the natural place to add a real runtime check (throwing a clear error if useAuth is somehow called outside an AuthProvider) instead of silently returning the context's bare default value and failing confusingly somewhere else, later.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What problem does React Context actually solve?

2. Why is context NOT a good replacement for plain props when a value is only needed by a component's direct children?

3. Why does bundling multiple unrelated state values (like user and theme) into one context object cause a real performance problem?