useContext Internals: How Context Propagates Without Props

useContext reads the nearest matching Provider value above it in the tree and subscribes to changes. Understanding how React finds that value, and what triggers re-renders, is the foundation for using context correctly.

June 7, 20263 min read1 / 2

Consider a parent component that holds a boolean toggle. It has two children: one that changes the value, one that displays it. Both need access to the same state.

The instinctive solution is passing it down as props:

TSX
function ContextExample() { const [isToggle, setIsToggle] = useState(false); return ( <> <ChildToggle setIsToggle={setIsToggle} /> <ChildDisplay isToggle={isToggle} /> </> ); }

That works for two levels. But add a grandchild that needs isToggle, and you have to pass it through ChildDisplay even though ChildDisplay itself does not use it — only to forward it one level deeper.

That is prop drilling: threading a value through intermediate components that do not need it, purely to get it where it is needed.

Prop drilling has two costs. It clutters intermediate components with props they never use. And every component receiving the prop re-renders when it changes — even the ones that only pass it along.

createContext: Creating a Shared Store

The fix is to lift shared state into a context object and let any descendant read it directly, without threading it through the tree.

Step one: create the context.

TSX
const GlobalStateContext = createContext(null);

createContext takes an optional default value — used only when a component calls useContext outside of any matching Provider. For most apps, null is fine here.

Step two: wrap the subtree in a Provider and pass the shared values:

TSX
function ContextExample() { const [isToggle, setIsToggle] = useState(false); return ( <GlobalStateContext.Provider value={{ isToggle, setIsToggle }}> <ChildToggle /> <ChildDisplay /> </GlobalStateContext.Provider> ); }

Everything inside the Provider — at any depth — can now read from that context. No props required. The value prop is what gets distributed.

Reading Context with useContext

In any descendant, call useContext with the context object:

TSX
function ChildToggle() { const { setIsToggle } = useContext(GlobalStateContext); return ( <button onClick={() => setIsToggle(prev => !prev)}> Toggle </button> ); } function ChildDisplay() { const { isToggle } = useContext(GlobalStateContext); return <p>{isToggle ? 'On' : 'Off'}</p>; }

No props. No intermediate forwarding. A grandchild at any depth can call useContext(GlobalStateContext) directly and get the same values — regardless of how many layers separate it from the Provider.

Context replaces prop drilling by making state available to the entire subtree, not just direct children.

How React Finds the Right Provider

useContext walks up the component tree and finds the nearest Provider for the given context object. If there are nested Providers of the same context (useful for scoped overrides), the nearest one wins.

If no Provider is found above the component, useContext returns the default value passed to createContext. This is why exporting the context object and importing it into every consumer file is the standard pattern — each file calls useContext with the same reference and React matches them to the correct Provider.

Exporting and Reusing Across Files

In a real project, components live in separate files. Export the context at the module level so all consumers can import the same reference:

TSX
// context/GlobalStateContext.ts export const GlobalStateContext = createContext(null); // components/ChildToggle.tsx import { GlobalStateContext } from '../context/GlobalStateContext'; function ChildToggle() { const { setIsToggle } = useContext(GlobalStateContext); // ... }

The Provider typically lives at the top of your component tree — App.tsx or a layout wrapper — so that every component in the app has access to it.

Practice what you just read.

Context Value QuizBuild a Theme Context
2 exercises