Usecontext

Prop drilling is fine until it is not -- useContext gives sibling components a shared channel without threading props through every layer in between.

May 15, 20265 min read7 / 7

Cart state in the previous post lives in Order.jsx and flows down to Cart.jsx as a prop. That works. But there is a problem hiding in the next feature: a navigation header that shows how many items are in the cart.

Header is a sibling of Order, not a descendant. There is no prop path from Order to Header. The options are: lift cart state up to App, thread it through every intermediate component, or use context.

When Context is the Right Tool

Prop drilling -- passing data through intermediate components that do not use it themselves -- is a real cost. It makes components harder to reuse and creates noise in interfaces that should not care about the data passing through them.

Context is the solution, but it comes with a tradeoff: it makes data flow invisible. When a component reads from context, there is no import, no explicit prop, nothing in the JSX that shows where the data came from. That indirection makes bugs harder to trace.

The rule of thumb: use context for app-level state that legitimately affects unrelated parts of the UI. The user's identity, the current theme, and a persistent shopping cart are all good candidates. Component-local state and data shared between a parent and its children are not.

Creating a Context

Create src/contexts.jsx. Putting all context definitions in one file is a pattern that scales well -- context values tend to be small and having them together makes the app-level state visible at a glance:

JSX
import { createContext } from "react"; export const CartContext = createContext([[], () => {}]);

createContext takes an initial value. This value is only used when a component reads context outside of any provider -- which usually means a bug. Providing the correct shape anyway ([array, function]) means TypeScript can infer the types and tools can autocomplete correctly.

Providing Context

In App.jsx, wrap the app with CartContext.Provider and pass the cart state in as value:

JSX
import { useState } from "react"; import { CartContext } from "./contexts"; import Header from "./Header"; import Order from "./Order"; const App = () => { const cartHook = useState([]); return ( <CartContext.Provider value={cartHook}> <Header /> <Order /> </CartContext.Provider> ); };

Notice cartHook is the raw result of useState([]) -- not destructured. useState returns [state, setter]. By passing the whole tuple as the context value, any component that reads this context gets both the current cart and the setter.

In React 19 the .Provider suffix can be dropped -- <CartContext value={cartHook}> works directly.

CartContext.Provider wraps the whole app; Order reads setCart, Header reads cart.length ExpandCartContext.Provider wraps the whole app; Order reads setCart, Header reads cart.length

Reading Context in Order

Order.jsx no longer owns its own cart state. Replace the useState call with useContext:

JSX
import { useContext } from "react"; import { CartContext } from "./contexts"; // inside Order: const [cart, setCart] = useContext(CartContext);

Everything else in the component stays the same. cart and setCart behave exactly as before -- the only difference is where they come from.

Reading Context in Header

Header.jsx needs the cart length for the nav badge. It does not need the setter:

JSX
import { useContext } from "react"; import { CartContext } from "./contexts"; const Header = () => { const [cart] = useContext(CartContext); return ( <nav> <span>Padre Gino's Pizza</span> <div className="nav-cart"> 🛒 <span className="nav-cart-number">{cart.length}</span> </div> </nav> ); };

Destructuring only [cart] and leaving out the setter is a readable signal that this component is read-only with respect to the cart.

Now when Order updates the cart via setCart, Header re-renders automatically because they share the same context value -- even though neither is a parent of the other.

The Persistence Benefit

Cart state in Order.jsx would disappear if the user navigated away from the Order page (unmounting the component clears all its state). Cart state in App.jsx via context persists for the lifetime of the app. Navigation between pages does not unmount App, so the cart survives.

The Tradeoff

With props, tracing data is mechanical: follow the prop from parent to child. With context, you have to search the codebase for CartContext to understand what modifies it. The indirection is the cost of the power.

This is why context should be reserved for state that genuinely belongs at the app level, not used as a shortcut to avoid prop drilling in cases where a couple extra props would suffice. The same warning applies when libraries like TanStack Router or TanStack Query use context internally -- that is their problem, not yours. How those libraries work is covered in the TanStack Router post.

The Essentials

  1. createContext(defaultValue) creates a context object. The default value is used only outside a provider.
  2. Context.Provider value={...} makes the value available to all descendants. Pass the full useState tuple [state, setter] so consumers can both read and write.
  3. useContext(MyContext) reads the nearest matching provider's value. No prop needed -- the connection is implicit.
  4. Use context for app-level state: authentication, theme, persistent cart. Avoid it for component-local or parent-to-child data flow where props are clearer.
  5. Context values persist as long as the provider is mounted. Moving state up to App via context prevents it from vanishing on navigation.

Further Reading and Watching