Passing data deeply with context
Share a value with every component below a point in the tree, without threading props through each layer.
Props are the normal way to pass information from a parent to a child, and they are great: you can see exactly where every value comes from. But some values are needed everywhere. The current theme, the signed-in user and the preferred language are all examples. Passing those through every component, including the ones in the middle that never use them, gets tiring fast.
React has a feature for exactly this case, called context. It lets a component make a value available to everything below it in the tree, no matter how deep.
The problem: prop drilling
Here is a small app where only ThemedButton cares about the theme. Yet Page and Toolbar both have to accept a theme prop, only to hand it down.
export default function App() {
const [theme, setTheme] = useState<Theme>('light');
return <Page theme={theme} />;
}
function Page({ theme }: { theme: Theme }) {
return <Toolbar theme={theme} />; // does not use it, only passes it on
}
function Toolbar({ theme }: { theme: Theme }) {
return <ThemedButton theme={theme} />; // same here
}This is called prop drilling: a prop is drilled through layers that do not need it. With two layers it is fine. With ten layers, or with five different values, every refactor turns into a scavenger hunt.
Creating, providing and reading context
Using context takes three steps:
- Create a context with
createContext(defaultValue), usually in its own file. - Provide a value by wrapping part of your tree in
<ThemeContext value={...}>. - Read it in any component below with
useContext(ThemeContext).
Toggle the checkbox. ThemedButton re-renders with the new theme, even though Toolbar never received a prop. When the value in a provider changes, React re-renders every component that reads that context.
A few details worth knowing:
- In React 19 you render the context itself as the provider:
<ThemeContext value={theme}>. Older code writes<ThemeContext.Provider value={theme}>. Both work today, and the shorter form is the modern one. useContextlooks up the tree for the nearest provider of that context. If it finds none, it returns the default value you passed tocreateContext.createContext<Theme>('light')is TypeScript telling the context what kind of value it carries.
The nearest provider wins
Because useContext finds the closest provider above it, you can override a context for one part of the tree. Here the sidebar is always dark, whatever the rest of the page uses.
Footer is outside the inner provider, so it still reads 'light'.
Typing context safely
The theme has a sensible default. Many contexts do not. What is the "default signed-in user" or the "default list of books"? There is none. The usual TypeScript pattern is:
- Create the context with
nullas the default:createContext<User | null>(null). - Export a small custom hook that reads the context and throws a clear error if there is no provider.
Components then call useUser() and always get a real User, never null.
Notice two more things in UserContext.tsx:
children: ReactNodeis how you type thechildrenprop.ReactNodemeans "anything React can render": elements, strings, numbers,null, or arrays of those.- The provider is wrapped in its own
UserProvidercomponent, which owns the state.Appstays tidy, and the context object itself is not exported, so the only way to use it is throughuseUser. You will use this exact pattern for your books in the next lesson.
Deep diveWhen not to use context
Context is tempting once you know it, but props are still the default. Before you reach for context:
- Try passing props. Explicit props make data flow obvious. Passing a prop through two or three components is normal, not a smell.
- Try passing JSX as
children. IfLayoutonly forwardspoststoPosts, let the parent render<Layout><Posts posts={posts} /></Layout>instead. NowLayoutdoes not need the prop at all.
Good uses of context are values that many distant components need: theme, current user, routing, and app-wide state like your book collection.
A compact-view switch
Add a "Compact view" checkbox to App. When it is on, every BookCard hides the author line. BookCard is rendered by BookList, which should not receive any new prop. Create a CompactContext with a default of false.
Show solution
import { createContext } from 'react';
export const CompactContext = createContext(false);import { useState } from 'react';
import { BookList } from './BookList';
import { CompactContext } from './CompactContext';
export default function App() {
const [compact, setCompact] = useState(false);
return (
<>
<label>
<input type="checkbox" checked={compact} onChange={(e) => setCompact(e.target.checked)} />{' '}
Compact view
</label>
<CompactContext value={compact}>
<BookList />
</CompactContext>
</>
);
}import { useContext } from 'react';
import { CompactContext } from './CompactContext';
interface BookCardProps {
title: string;
author: string;
}
export function BookCard({ title, author }: BookCardProps) {
const compact = useContext(CompactContext);
return (
<article className="card">
<h2>{title}</h2>
{!compact && <p>by {author}</p>}
</article>
);
}BookList did not change at all. Because the default is false, TypeScript infers the context type as boolean, so no generic is needed.
A component calls useContext(ThemeContext), but there is no ThemeContext provider anywhere above it. What does it get?
Which situation is the best fit for context?
Day 5 progress