Context, and the end of prop drilling
March 7, 2022 · 5 min read
My login form needed to tell the rest of the app who was logged in. The shop and the cart needed that too, and none of those components had anything to do with each other. I knew the textbook answer, but writing it out showed me how clumsy it is.
The prop chain
The usual fix is to lift the state up. The closest shared parent, App, owns the login data and the handlers, and hands them down as props. That works until the component that needs the value sits three levels below the one that has it.
In my small app, MainHeader received isAuthenticated only to give it straight to Navigation. It never looked at it. Every layer in between just carries the prop along, and that is what people call prop drilling.
Making a context
React has a built-in place to keep state that any component can reach. You create it once, in its own file:
const AuthContext = React.createContext({
isLoggedIn: false,
});
export default AuthContext;What comes back is not a component. It is an object that has components on it, and the one I need is AuthContext.Provider. Whatever I wrap in the provider can read the context. Anything outside it cannot.
The default is not the value
This tripped me up. I wrapped everything in <AuthContext.Provider> without a value, read the context in Navigation, and the app crashed. The default from createContext is only used when there is no provider above you. Once there is one, it replaces the default, even if it has nothing in it.
So the provider gets the real value, and it is a live one because it comes from state:
<AuthContext.Provider
value={{ isLoggedIn: isLoggedIn, onLogout: logoutHandler }}
>
<MainHeader />
<main>...</main>
</AuthContext.Provider>The prop has to be called value. When the state in App changes, the new object goes to every component that is listening.
Reading it
There are two ways. The old one is AuthContext.Consumer, which takes a function as its child and gives you the value as an argument. It works, but it adds a layer of nesting to the JSX. The hook is the same thing with none of that:
const Navigation = (props) => {
const ctx = useContext(AuthContext);
return (
<nav>
{ctx.isLoggedIn && <button onClick={ctx.onLogout}>Logout</button>}
</nav>
);
};Now I could delete the props from MainHeader altogether. App renders <MainHeader />, MainHeader renders <Navigation />, and Navigation gets what it needs by itself.
When I still use props
Login and Home still get their handlers as props from App, because they use them directly and don't pass them on. The Button inside Home also takes its onClick as a prop. If Button read the logout handler from context, I could never use it for anything else.
Props are how a component gets configured and reused. Context is for the specific case where something is being forwarded through many layers that don't care about it.
Where it falls short
Context is not built for values that change many times a second. When the provider gets a new value, every component that reads that context re-renders. A component can't say it only cares about one field of the object. For a login flag that flips now and then, that is nothing. For fast-moving state, I would need something else.
What stuck with me: context is a shortcut around components that only pass things along, not a replacement for props.