Context, and hooks of your own
Passing a value past the components in the middle, wrapping your own logic in a hook, and the two rules every hook obeys.
The drilling problem
A theme, the current user, a language — some values are wanted deep in the tree by components far from where they are held. Passing each one down through every level means components in the middle take props they do not use, only to hand them on. That is prop drilling, and it makes the middle of the tree impossible to move.
Context removes the middle from the trip. createContext makes the channel, a Provider at the top fills it, and any component below reads it with useContext — however many levels away, without a single prop in between.
Using it, and not overusing it
A component reading context re-renders when the value changes, so the value should change rarely — a theme, a session, a language. Rapidly changing values are better passed as props, where it is obvious what depends on them.
And context is not a state manager. It does not hold anything by itself: a useState in the provider above is still the state, while the context is only the pipe that distributes it. Reaching for context before there is anything to drill past is a way of writing props with extra steps.
A hook of your own
When two components need the same stateful behaviour, that behaviour can be a function of its own — a custom hook: a function whose name starts with use and which calls other hooks inside. useToggle hides the state and returns what the caller cares about: whether it is on, and a way to flip it.
Two rules make hooks what they are. They are called at the top level of a component or of another hook — never inside a condition, a loop, or a nested function, because React matches each call by order. And only components and hooks may call them. Both rules are about the same thing: the order of the calls is the identity of the state, so it has to be the same on every render.
A hook returns values, not markup
A custom hook is a function, not a component: its name starts with use, it calls other hooks, and it returns whatever the caller needs — a value, a pair, an object of handlers. It returns no markup, because it is not part of the tree. The component that calls it still owns the render.
That separation is what makes the logic testable and reusable: the hook can be called from two components with different markup and behave the same way, and neither component has to know how the state is stored.
A channel through the middle, a hook of your own
const { useState, useContext, createContext } = React;
const ThemeContext = createContext('light');
// Reads the channel — no theme prop was passed to it.
function Chip() {
const theme = useContext(ThemeContext);
return <span className={'chip chip--' + theme}>theme: {theme}</span>;
}
// The component in between knows nothing about the theme.
function Toolbar() {
return <div className="toolbar"><Chip /></div>;
}
function useToggle(start) {
const [on, setOn] = useState(start);
return [on, () => setOn((value) => !value)];
}
function App() {
const [dark, toggleDark] = useToggle(false);
return (
<ThemeContext.Provider value={dark ? 'dark' : 'light'}>
<h1>Context</h1>
<Toolbar />
<button onClick={toggleDark}>switch theme</button>
</ThemeContext.Provider>
);
}The nearest provider wins
createContext(defaultValue) returns a context object with a Provider component, and useContext reads it. React does not store the value globally: each Provider puts a value in the tree, and useContext walks up from the component that called it until it finds the nearest one. No provider means the default.
The value is compared by identity. A Provider that builds { user, setUser } inline creates a new object every render, so every consumer re-renders even when the fields did not change. A stable value — or state that changes rarely — keeps context cheap.
Hooks share logic, not state
A custom hook is an ordinary function that calls other hooks; the use prefix is the signal that it does. Call useToggle() in two components and each call gets its own useState — the logic is shared, the state is not. That is the opposite of context, which shares one value.
Hooks compose: useProgress can call useContext, and a component can call both. The rules do not change — top level, same order, components and hooks only — because React still identifies each hook by its position in the call sequence.
Three context mistakes: creating the context inside a component, which makes a new channel on every render; calling a hook after an early return, so the call order changes between renders; and expecting context to hold state — it only carries whatever the provider was given, and a useState above it is still the owner.
Try these next
- Add a second consumer below two wrapper components and confirm it needs no props.
- Make the provider pass a stable object and explain which consumers re-render when
readchanges. - Write a
useRemaininghook that returns the number of unread chapters, and use it in the bar.
A theme that skips the middle
Run it and flip the theme. Then add a `Card` component between Toolbar and Chip, with no props at all, and confirm the chip still knows the theme.
A reading bar that skips the middle
Run it and mark chapters read. Then add a second consumer below another wrapper with no props, and confirm both update. Finish by writing a `useRemaining` hook that returns the number of unread chapters and showing it in the bar.
A hook after an early return breaks the rules quietly: the first render calls two hooks, the second calls one, and the state lands on the wrong hook. Keep every hook above every return.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What problem does context solve?
