useContext: data for the whole app
The problem: props through ten levels
Data in React flows top down through props. Usually that's good: you can see where everything came from. But some data is needed by almost all components: the current user, the theme, the language, the cart. Passing it through props has to go through every intermediate component — even those that don't need it:
function App() {
const [user, setUser] = useState({ name: "Ann" });
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />; // Layout doesn't use user itself
}
function Header({ user }) {
return <Menu user={user} />; // and neither does Header
}
function Menu({ user }) {
return <p>Hello, {user.name}</p>; // it's only needed here
}
This is called prop drilling. The code becomes fragile: to add a new value, you have to change all the intermediate components.
Context lets a component at the top "declare" a value, and any component below read it directly, without passing it through the intermediate levels.
Three steps: create, provide, read
const { createContext, useContext } = React;
// 1. Create the context (usually in a separate file). The argument is the default value
const UserContext = createContext(null);
// 2. Provide the value: wrap part of the tree in a Provider
function App() {
const [user, setUser] = useState({ name: "Ann" });
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function Layout() {
return <Header />; // no props
}
function Header() {
return <Menu />;
}
// 3. Read it in any component inside the Provider
function Menu() {
const user = useContext(UserContext);
return <p>Hello, {user.name}</p>;
}
createContext(defaultValue)creates a context object.<Ctx.Provider value={...}>makes the value available to all components inside, at any depth.useContext(Ctx)returns the value of the nearest Provider above in the tree. If there's no Provider — the default value.
When the value changes
When a Provider's value changes, React re-renders all the components that read this context via useContext —
even if their parents didn't re-render. That's how context stays up to date throughout the application.
Providers can be nested: the inner one "overrides" the outer one for its part of the tree.
<ThemeContext.Provider value="dark">
<Page /> {/* the theme is dark here */}
<ThemeContext.Provider value="light">
<Footer /> {/* and light here */}
</ThemeContext.Provider>
</ThemeContext.Provider>
Context + state
Context by itself stores nothing — it only passes a value down. To make the data changeable, context is combined with state, and both the value and the functions to change it are passed down:
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function LoginButton() {
const { user, login, logout } = useContext(AuthContext);
return user
? <button onClick={logout}>Log out ({user.name})</button>
: <button onClick={() => login("Ann")}>Log in</button>;
}
function App() {
return (
<AuthProvider>
<Header />
<LoginButton />
</AuthProvider>
);
}
This is the standard pattern: a "provider" component with state inside and children outside. Through children the
whole application ends up inside the Provider.
A custom hook for the context
Projects almost always make a small wrapper hook. It's shorter, and it reports an error right away if a component accidentally ends up outside the provider:
function useAuth() {
const value = useContext(AuthContext);
if (value === null) {
throw new Error("useAuth must be used inside AuthProvider");
}
return value;
}
function Profile() {
const { user } = useAuth(); // instead of useContext(AuthContext)
return <p>{user ? user.name : "Guest"}</p>;
}
A hook is an ordinary function whose name starts with use and inside which other hooks are called. More on custom
hooks in the data loading topic.
When you don't need context
Context is handy, but you shouldn't put everything in it:
- if the data is needed one or two levels down — pass props, it's simpler and clearer;
- composition often helps: instead of passing data through
Layout, pass a ready-made element throughchildren—<Layout><Menu user={user} /></Layout>; - every change of
valuere-renders all readers. For frequently changing data (the mouse position, the text in a field) context is a poor choice.
Good candidates for context: the current user, the theme, the language, the cart, settings.
Common mistakes
- A component outside the Provider.
useContextsilently returns the default value (null), and somewhere later you getCannot read properties of null. A hook with a check gives a clear error right away. - Passing the wrong object to
useContext. You needuseContext(ThemeContext), notuseContext(ThemeContext.Provider)or a string. - Forgetting
value.<Ctx.Provider>withoutvaluepassesundefined. - Expecting that changing an object inside
valueupdates the screen. Same as with state: you need a new object viaset….
Summary
- Context passes data deep into the tree without props:
createContext→<Ctx.Provider value>→useContext(Ctx). useContexttakes the value of the nearest Provider above, otherwise the default value.- When
valuechanges, all readers of the context re-render. - Context +
useStatein a provider component = shared changeable application state. - A custom
useX()hook with anullcheck is handy and safe. - For data one or two levels down — ordinary props.