Skip to Content
React in a Week
Day 5 · Scaling up (TypeScript)

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:

  1. Create a context with createContext(defaultValue), usually in its own file.
  2. Provide a value by wrapping part of your tree in <ThemeContext value={...}>.
  3. Read it in any component below with useContext(ThemeContext).
Live code· Theme context

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.
  • useContext looks up the tree for the nearest provider of that context. If it finds none, it returns the default value you passed to createContext.
  • 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.

Live code· Overriding a context

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:

  1. Create the context with null as the default: createContext<User | null>(null).
  2. 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.

Live code· A typed context with a custom hook

Notice two more things in UserContext.tsx:

  • children: ReactNode is how you type the children prop. ReactNode means "anything React can render": elements, strings, numbers, null, or arrays of those.
  • The provider is wrapped in its own UserProvider component, which owns the state. App stays tidy, and the context object itself is not exported, so the only way to use it is through useUser. 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. If Layout only forwards posts to Posts, let the parent render <Layout><Posts posts={posts} /></Layout> instead. Now Layout does 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.

Try it

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.

Live code· Challenge: compact view
Show solution
CompactContext.tsx
import { createContext } from 'react';

export const CompactContext = createContext(false);
App.tsx
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>
    </>
  );
}
BookCard.tsx
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.

Quick check

A component calls useContext(ThemeContext), but there is no ThemeContext provider anywhere above it. What does it get?

Quick check

Which situation is the best fit for context?

Day 5 progress

0/7

On this page