Context lets a component read a value from far above it, with no props in between. Learn how to give and read it, who re-renders, and the traps.
In Part 3 we gave a problem a name: prop drilling. A value has to pass down through many layers of components. The middle ones don’t use it. They only pass it on. Part 9 met it again, when state lived far above the components that needed it.
React has a tool for this. It is called context. A component high up gives a value, and any component below it can read that value. The components in between don’t need to know about it.
Earlier parts touched context a few times. Part 18 named a context change as one of the things that make a component render. Part 20 showed that memo can’t stop it. This part covers context properly. You’ll learn how to make one, give a value and read it. You’ll see which value wins and who renders when it changes. And you’ll meet the common mistakes.
Try this first
Read this code. Don’t press Run yet.
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function ThemeButton({ label }: { label: string }) {
const theme = useContext(ThemeContext)
return <button>{label}: {theme}</button>
}
export default function App() {
return (
<div>
<ThemeButton label="Outside" />
<ThemeContext value="dark">
<ThemeButton label="Inside" />
</ThemeContext>
</div>
)
}
Both buttons are the same component. Both read the same ThemeContext. Only the second one sits inside the <ThemeContext value="dark"> tag.
Make a guess. Will both buttons say “dark”? Will both say “light”? Or will they say different things?
Now press Run.
The first button says “Outside: light”. The second says “Inside: dark”. The same component read the same context and got two different answers. What it got depends on where it is on the page. That is the whole idea of context, and the rest of this part explains it piece by piece.
The problem: prop drilling
Here is a small page. App knows the theme. Only ThemeButton, at the bottom, needs it. So the theme travels down through Page and Toolbar.
function ThemeButton({ theme }: { theme: string }) {
return <button>Theme: {theme}</button>
}
function Toolbar({ theme }: { theme: string }) {
return (
<nav>
<ThemeButton theme={theme} />
</nav>
)
}
function Page({ theme }: { theme: string }) {
return (
<main>
<h1>Welcome</h1>
<Toolbar theme={theme} />
</main>
)
}
export default function App() {
const theme = 'dark'
return <Page theme={theme} />
}
It works. But look at Page and Toolbar. They take a theme prop and do nothing with it except pass it on. With three layers, that is fine. With ten layers, and five values, every middle component gets a long list of props it doesn’t use. Adding one more value means changing every layer.
That is prop drilling. Context removes the middle steps.
Context in three steps
Here is the same page with context. The page looks exactly the same.
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button>Theme: {theme}</button>
}
function Toolbar() {
return (
<nav>
<ThemeButton />
</nav>
)
}
function Page() {
return (
<main>
<h1>Welcome</h1>
<Toolbar />
</main>
)
}
export default function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
)
}
Page and Toolbar have no props now. We checked: both versions put exactly the same HTML on the page.
There are three steps.
- Make the context.
createContext('light')makes a context object. Call it once, at the top level of a file, outside any component. The name ends inContextby habit, so you can see what it is. - Give a value.
<ThemeContext value="dark">wraps part of the page. Every component inside it, at any depth, can now read"dark". A tag that gives a context value like this is called a provider. - Read the value.
useContext(ThemeContext)is a hook. It gives back the value from the provider above this component.
React’s page on createContext says the context object “does not hold any information”. It is like a name tag for the value. It holds only the default value. The provider holds the current value. useContext asks React: “Find the closest ThemeContext provider above me. What value does it give?”
The default value
The 'light' in createContext('light') is the default value. A component gets it only when no provider of that context is anywhere above it.
That is what happened to the “Outside” button in “Try this first”. Nothing above it gave ThemeContext a value. So useContext returned 'light'.
You can see it in the three-step example too. Press Edit, and change the end of App so it returns only <Page />, with no <ThemeContext> around it. Run it. The button now says “Theme: light”.
React’s docs say the default is for when nothing else is found. They also say “It is static and never changes over time.” A default value is useful in two places. A component still works when no provider is above it. And a test can show the component without setting up providers first.
There is a catch. The default is used only when there is no provider at all. We checked: a provider with value={undefined} gives undefined, not the default. A provider with value={null} gives null. React treats any provider above as the answer, even one whose value is undefined or null.
Giving a value: two ways to write the provider
In React 19, the context object itself is the provider tag: <ThemeContext value="dark">. This is what React’s docs use now, and what this series uses.
Code written for React 18 and before uses a longer form. You will see it in many projects and lessons:
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button>Theme: {theme}</button>
}
export default function App() {
return (
<ThemeContext.Provider value="dark">
<ThemeButton />
</ThemeContext.Provider>
)
}
Run it. It works the same, with no warning. We checked why: in React 19.3, ThemeContext.Provider is the very same object as ThemeContext.
You may also meet <ThemeContext.Consumer> in old code. It is an older way to read a context. React’s docs call it “an alternative and rarely used way to read the context value”. Use useContext instead.
React’s createContext page calls SomeContext.Provider “a legacy way to provide the context value before React 19”. Legacy means old, and kept only so old code keeps working. The React 19 blog post says: “In future versions we will deprecate <Context.Provider>.” To deprecate something means to mark it as “don’t use this any more”. It may be removed one day. So write new code with <ThemeContext value>, and don’t be surprised by .Provider in older code.
The value can be anything: a string, a number, an object, a function. Use curly braces for anything that isn’t fixed text: value={theme}.
The nearest provider wins
You can put a provider inside another provider of the same context. A component then gets the value of the nearest one above it.
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function Box({ label }: { label: string }) {
const theme = useContext(ThemeContext)
return <p>{label} is {theme}</p>
}
export default function App() {
return (
<ThemeContext value="dark">
<Box label="Header" />
<ThemeContext value="light">
<Box label="Menu" />
<ThemeContext value="blue">
<Box label="Ad" />
</ThemeContext>
</ThemeContext>
<Box label="Footer" />
</ThemeContext>
)
}
Run it. The page shows:
- “Header is dark”. Only the outer provider is above it.
- “Menu is light”. The middle provider is closer.
- “Ad is blue”. The inner provider is the closest of all three.
- “Footer is dark”. It comes after the middle provider closes, so only the outer one is above it again.
This is how one part of a page can look different from the rest. A dark page can have a light menu. You wrap that part in its own provider, and it replaces the value for that part only.
Different contexts don’t mix. A LanguageContext provider doesn’t change what ThemeContext gives. Each context made with createContext is separate. One component can read many contexts.
An everyday example
Think of a school with a speaker in every room. The office speaks once: “No school today.” Every room with a speaker hears it. The office doesn’t walk to each room, and the rooms on the way don’t pass the message along.
One building of the school can have its own speaker system. When it plays its own message, the rooms in that building hear that one instead.
The office is the provider. The message is the value. A room that listens is a component that calls useContext. The building with its own system is a nested provider.
The exact version
The speaker idea breaks in three places.
- A message from a speaker reaches every room. React sees your components as a tree. One is at the top, its children are under it, and their children are under them. A provider’s value reaches only the components inside it, below it in the tree. Anything outside gets the default value.
- A room hears the message only if it listens. In React, only the components that call
useContext(oruse, below) read the value. The rest don’t even notice it. - When the message changes, every room that listens hears it again. In React, every component that reads the context renders again. That is the next big topic.
Reading context with use
React 19 added a second way to read a context: use. React’s docs say it “works similarly to useContext“. The difference: “Unlike useContext, use can be called within loops and conditional statements like if.”
import { createContext, use, useState } from 'react'
const ThemeContext = createContext('light')
function Note({ open }: { open: boolean }) {
if (!open) {
return <p>The note is closed.</p>
}
const theme = use(ThemeContext)
return <p>The note is open. Theme: {theme}</p>
}
export default function App() {
const [open, setOpen] = useState(false)
return (
<ThemeContext value="dark">
<button onClick={() => setOpen(!open)}>Open or close</button>
<Note open={open} />
</ThemeContext>
)
}
Run it and click the button. Note reads the theme only when it is open. React’s docs say useContext “must be called at the top level of your component”, like every hook. That means not inside an if or a loop, and not after an early return. use can go inside an if or a loop. It still has rules. The docs say it “must be called inside a Component or a Hook”. They also say it “cannot be called inside a try-catch block”.
What if you write useContext in place of use here? In a small test, it seemed to work. But it breaks the Rules of Hooks, and a linter catches it. Oxlint, in the Part 10 project, gave this error:
x react-hooks(rules-of-hooks): React Hook "useContext" is called conditionally. React Hooks must be called in the exact same order in every component render.
With use, the same linter found no errors.
use can also read a Promise, which is a value that arrives later. That is a different topic, and Part 31 covers it. For context, use and useContext find the value in the same way. They take the closest provider above, or the default if there is none. In this series we use useContext unless we need use inside an if.
When the value changes
A context value can come from state. Then it can change. What happens to the components below?
This example has four components under the provider. Look at which ones read the context:
Pageis wrapped inmemo(Part 20) and takes no props. It doesn’t read the context.Toolbaris insidePage. It doesn’t read the context either.ThemeLabelreads it withuseContext.Iconis a child ofThemeLabel. It doesn’t read the context.
import { createContext, memo, useContext, useState } from 'react'
const ThemeContext = createContext('light')
const Page = memo(function Page() {
console.log('Page renders')
return (
<main>
<Toolbar />
</main>
)
})
function Toolbar() {
console.log('Toolbar renders')
return (
<nav>
<ThemeLabel />
</nav>
)
}
function ThemeLabel() {
const theme = useContext(ThemeContext)
console.log('ThemeLabel renders')
return (
<p>
Theme: {theme} <Icon />
</p>
)
}
function Icon() {
console.log('Icon renders')
return <span>(icon)</span>
}
export default function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext value={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Switch theme</button>
<Page />
</ThemeContext>
)
}
Page renders
Page renders
Toolbar renders
Toolbar renders
ThemeLabel renders
ThemeLabel renders
Icon renders
Icon renders
ThemeLabel renders
ThemeLabel renders
Icon renders
Icon renders
Make a guess before you click. When the theme changes, which of the four will log?
Press Run, then click “Switch theme” once.
Every line is doubled. That is Strict Mode, which the playground turns on. While you develop, React calls each component twice to help find bugs. Part 2 explains why. So the first eight lines are the first render: four components, twice each.
After the click, only four more lines appear: ThemeLabel renders twice and Icon renders twice. Page and Toolbar did not render. We measured the same with Strict Mode off. One click rendered ThemeLabel once and Icon once. Nothing else below the provider rendered.
The value goes past the components that don’t read it. On a change, only the readers render (and their children). Page in memo stops the rest. Press play, or step through it.
Here are the same steps in words.
Appholds the theme in state and gives it with<ThemeContext value={theme}>.- The value goes past
PageandToolbar. They don’t read it, so they don’t need it as a prop. ThemeLabelreads it withuseContextand shows “light”.- You click “Switch theme”.
setThememakesApprender again, now with “dark”. Pageis wrapped inmemoand has no props, so they are the same. React skipsPage, and so it skipsToolbarinside it too.- React finds the components that read
ThemeContext.ThemeLabelis one, so it renders.Iconrenders too, because its parent rendered and it is not wrapped inmemo. - React commits, which means it puts the result on the page. The page shows “Theme: dark”.
So the rule is: every component that reads a context renders again when its value changes. It doesn’t matter how deep it is, or whether the components above it were skipped.
Even through memo
Part 20 showed this already, and it is worth checking here too. Press Edit and wrap ThemeLabel in memo as well. Run it and click. We measured one click: ThemeLabel still rendered, and so did Icon. React’s useContext page says it plainly: “Skipping re-renders with memo does not prevent the children receiving fresh context values.”
How React decides the value “changed”
React compares the old value and the new value with Object.is. Part 20 explained it. For strings and numbers, it compares the values. For objects, it asks: “is this the very same object?” The useContext page says: “The previous and the next values are compared with the Object.is comparison.”
We checked this. Press Edit and add a second button to App that changes some other state, like a click counter. A click renders App, so the provider runs again. But it gets the same string, 'light'. We measured three such clicks. App rendered each time, and no component below the provider logged anything. Fix 2 below shows the same thing with its “Clicks” button.
What memo did here
Without memo on Page, the picture is different. App renders, so everything it writes renders too, as Part 18 showed. We measured it: one click then rendered Page, Toolbar, ThemeLabel and Icon. They didn’t render because of context. They rendered because their parent did.
So context doesn’t make anything render less by itself. It makes the readers render when the value changes. Whether the components between the provider and the readers render depends on the usual rules from Parts 18 to 20.
The object trap
Often a context holds more than one thing. A common case is the current user and a function to change it. The easy way to write that is an object, right in the JSX:
import { createContext, memo, useContext, useState } from 'react'
type UserInfo = { user: string; setUser: (name: string) => void }
const UserContext = createContext<UserInfo>({ user: 'Nobody', setUser: () => {} })
function Profile() {
const { user } = useContext(UserContext)
console.log('Profile renders')
return <p>User: {user}</p>
}
const Page = memo(function Page() {
return <Profile />
})
export default function App() {
const [user, setUser] = useState('Ana')
const [clicks, setClicks] = useState(0)
return (
<UserContext value={{ user, setUser }}>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<Page />
</UserContext>
)
}
Profile renders
Profile renders
Profile renders
Profile renders
createContext<UserInfo>(...) tells TypeScript what the value holds. The <UserInfo> part gives the type, as with useState in earlier parts. The default here is a user called “Nobody” and a function that does nothing.
Run it and click “Clicks”. The click has nothing to do with the user. The user is still “Ana”. But Profile rendered again: two lines for the first render, and two more for the click.
Here is why. {{ user, setUser }} makes a new object every time App renders. The outer braces mean “JavaScript here”, and the inner ones make an object. The click renders App, so the provider gets a new object. Object.is says a new object is not the same as the old one. It doesn’t matter that what is inside is the same. So React renders every reader.
We counted it. We added a log to App too. With Strict Mode off, three clicks rendered App 3 times and Profile 3 times. Profile saw 4 different value objects: one from the first render, and one from each click. With Strict Mode on, those 3 clicks logged “Profile renders” 6 times.
The playground doesn’t run the React Compiler from Part 22. We built this same code with the compiler and ran it with Strict Mode off. Three clicks rendered App 3 times, and Profile 0 times. The compiler kept the object, so we didn’t need a fix by hand.
In a small app this costs little, and React’s docs say so. But a big app can have many readers of one context. Each extra render of each reader adds up. Without the compiler, there are three fixes.
Fix 1: keep the object with useMemo
Part 21 showed useMemo. It usually keeps the answer of a calculation until its dependencies change. Part 21 explained that React may sometimes throw the kept answer away. Here the calculation makes the object:
import { createContext, memo, useContext, useMemo, useState } from 'react'
type UserInfo = { user: string; setUser: (name: string) => void }
const UserContext = createContext<UserInfo>({ user: 'Nobody', setUser: () => {} })
function Profile() {
const { user } = useContext(UserContext)
console.log('Profile renders')
return <p>User: {user}</p>
}
const Page = memo(function Page() {
return <Profile />
})
export default function App() {
const [user, setUser] = useState('Ana')
const [clicks, setClicks] = useState(0)
const value = useMemo(() => ({ user, setUser }), [user])
return (
<UserContext value={value}>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<Page />
</UserContext>
)
}
Profile renders
Profile renders
Run it and click “Clicks” a few times. “Profile renders” shows only twice, for the first render. We measured it with Strict Mode off: after the first render and three clicks, Profile had seen only 1 value object.
The dependency list is [user]. setUser doesn’t need to be in it. Part 20 explained that a set function from useState stays the same object on every render. When user changes, useMemo makes a new object, and Profile renders with the new name. That is the render we want.
This fix helps only because Page is wrapped in memo. We removed memo and measured again. With Strict Mode off, three clicks rendered Profile 3 times, even with useMemo. App rendered, so Page rendered, so Profile rendered because its parent did. Part 24 looks at this more closely.
React’s useContext page shows this same fix, with useMemo for the object and useCallback for a function inside it.
Fix 2: split the context
Some components only need to change the user. They never show it. A button that sets a new name is one. Say the user and the set function live in one object. Then that button still renders every time the user changes. We measured it with a memoized object, as in Fix 1. Changing the name rendered both Profile and RenameButton.
The fix is two contexts. One holds the user. The other holds the set function.
import { createContext, memo, useContext, useState } from 'react'
const UserContext = createContext('Nobody')
const SetUserContext = createContext<(name: string) => void>(() => {})
function Profile() {
const user = useContext(UserContext)
console.log('Profile renders')
return <p>User: {user}</p>
}
function RenameButton() {
const setUser = useContext(SetUserContext)
console.log('RenameButton renders')
return <button onClick={() => setUser('Ben')}>Make it Ben</button>
}
const Page = memo(function Page() {
return (
<div>
<Profile />
<RenameButton />
</div>
)
})
export default function App() {
const [user, setUser] = useState('Ana')
const [clicks, setClicks] = useState(0)
return (
<SetUserContext value={setUser}>
<UserContext value={user}>
<button onClick={() => setClicks(clicks + 1)}>Clicks: {clicks}</button>
<Page />
</UserContext>
</SetUserContext>
)
}
Profile renders
Profile renders
RenameButton renders
RenameButton renders
Profile renders
Profile renders
Run it. Click “Clicks”, then “Make it Ben”. Watch the Console.
- The first four lines are the first render.
- “Clicks” logs nothing. Neither context value changed.
- “Make it Ben” logs “Profile renders” twice.
RenameButtondoes not render. Its context holdssetUser, which is the same function every time.
Fix 3: keep values simple
Look again at Fix 2. Neither provider needs useMemo. UserContext holds a string, and Object.is compares strings by their value. SetUserContext holds setUser, which never changes. A context that holds a string, a number or a true/false value can’t fall into the object trap.
So, before you put an object in a context, ask: could this be one or two simple values instead?
TypeScript: a context with no good default
Sometimes no default makes sense. What is “the current theme” with no theme set up? A common choice is null, which means “nothing here”.
React’s TypeScript page shows the way. Give the type with | null, which means “this type, or null“. Then write a small custom hook (Part 16). It checks for null and throws a clear error:
import { createContext, useContext, useMemo, useState } from 'react'
type Theme = { color: string; setColor: (color: string) => void }
const ThemeContext = createContext<Theme | null>(null)
function useTheme() {
const theme = useContext(ThemeContext)
if (theme === null) {
throw new Error('useTheme must be used inside <ThemeContext>')
}
return theme
}
function ColorButton() {
const { color, setColor } = useTheme()
return (
<button onClick={() => setColor(color === 'red' ? 'blue' : 'red')}>
Color: {color}
</button>
)
}
export default function App() {
const [color, setColor] = useState('red')
const theme = useMemo(() => ({ color, setColor }), [color])
return (
<ThemeContext value={theme}>
<ColorButton />
</ThemeContext>
)
}
Run it and click the button. It changes from red to blue.
The hook does two jobs.
- For TypeScript. After the
if, TypeScript knowsthemecan’t benull. SouseTheme()returns aTheme, andColorButtoncan readcolorwith no extra checks. - For you. If someone uses
ColorButtonwith no provider above it, the error says exactly what is wrong.
Without the hook, TypeScript stops you. If ColorButton read the context directly, like this:
import { createContext, useContext } from 'react'
type Theme = { color: string; setColor: (color: string) => void }
const ThemeContext = createContext<Theme | null>(null)
function ColorButton() {
const { color } = useContext(ThemeContext)
return <button>Color: {color}</button>
}
TypeScript says: Property 'color' does not exist on type 'Theme | null'. It is telling you the value might be null.
Now see the hook’s error. Here App forgets the provider:
import { createContext, useContext } from 'react'
type Theme = { color: string; setColor: (color: string) => void }
const ThemeContext = createContext<Theme | null>(null)
function useTheme() {
const theme = useContext(ThemeContext)
if (theme === null) {
throw new Error('useTheme must be used inside <ThemeContext>')
}
return theme
}
function ColorButton() {
const { color, setColor } = useTheme()
return (
<button onClick={() => setColor(color === 'red' ? 'blue' : 'red')}>
Color: {color}
</button>
)
}
export default function App() {
return <ColorButton />
}
Press Run. The page shows no button. Instead, the playground shows the error: useTheme must be used inside <ThemeContext>. We measured the same message, with Strict Mode on and off. The message names the hook and the fix. That is much easier to act on than a crash with a strange message.
When to use context
Context is easy to use too much. React’s docs warn that this also makes it easy to use too much.
Before context, the docs say to try two things:
- Start by passing props. The docs say a dozen props through a dozen components is “not unusual”. Props make it very clear which component uses which data.
- Pass JSX as
children. If middle components only pass data along, you may be missing a component. ALayoutthat takeschildrendoesn’t need to know about the data inside. Part 19 used the same trick to save renders.
“If neither of these approaches works well for you, consider context.” The docs then list good uses:
- Theme. Dark mode and colors, read by many components.
- The current user. Many parts of the page need to know who is signed in.
- Routing. Which page of the app is showing. Most routing tools use context inside.
- Managing state. State near the top of the app that many far-away components change. React’s docs pair context with a reducer for this (Part 15).
The first three have something in common: many components read them. And in most apps, they don’t change often. That last point is our advice, not the docs’. A theme changes when someone clicks a switch. The user changes when someone signs in.
Be careful with a value that changes very often. The text in a search box changes on every key press. A mouse position changes all the time. You saw the rule above: every reader renders on every change. With many readers, that is a lot of rendering. React allows it. But think about who reads the value, and keep the context small.
What comes next
This part used context directly inside App. The next parts build on it.
- Part 24, the provider pattern, moves the state and the provider into a component of their own.
- Part 25 shows how a component can read only the part of a context it needs.
- Part 27 looks at global state, which means state that the whole app shares, without a library.
Common mistakes
No provider above, so the default is used
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button>Theme: {theme}</button>
}
export default function App() {
return <ThemeButton />
}
You meant the button to be dark. But no provider is above it, so it quietly shows the default, “light”. React prints no error and no warning. The fix is to wrap it: <ThemeContext value="dark"><ThemeButton /></ThemeContext>.
Is a missing provider always a bug in your app? Then use the null default and the useTheme hook from the TypeScript section. Then a missing provider gives a clear error instead of a quiet wrong answer.
A similar mistake is a provider with no value:
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button>Theme: {String(theme)}</button>
}
export default function App() {
return (
<ThemeContext>
<ThemeButton />
</ThemeContext>
)
}
TypeScript stops this: “Property ‘value’ is missing”. The playground doesn’t check types, so press Run. The button shows “Theme: undefined”, not the default. In development, React 19.3 prints this in the Console:
The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?
The message names <Context.Provider> even though the code wrote <ThemeContext>. We got the same warning for a wrong prop name, like theme="dark" instead of value="dark". The fix: <ThemeContext value="dark">.
Reading the context in the component that gives it
import { createContext, useContext } from 'react'
const ThemeContext = createContext('light')
function Panel() {
const theme = useContext(ThemeContext)
return (
<ThemeContext value="dark">
<p>Panel reads: {theme}</p>
</ThemeContext>
)
}
export default function App() {
return <Panel />
}
Run it. It says “Panel reads: light”, not “dark”. useContext looks for a provider above the component that calls it. A provider that this same component returns is below it. React’s docs say useContext “does not consider providers in the component from which you’re calling” it.
The fix: move the reading into a child component, like PanelText, and put <PanelText /> inside the provider. We checked: then it reads “dark”.
A new object as the value on every render
value={{ user, setUser }} makes a new object on every render, so every reader renders every time the provider’s component renders. You saw it in the object trap. Use useMemo, split the context, or keep the value simple.
One giant context for everything
You might make one AppContext that holds everything. The user, the theme, the language and the shopping cart all go in one object. Then a change to any of them gives a new object. Every component that reads AppContext renders, even one that only needs the language. We measured a small version. The user and its set function were in one memoized object. Changing the name also rendered RenameButton, which only used the set function. With two contexts, it didn’t.
Make one context per idea that changes on its own: ThemeContext, UserContext, CartContext. A component reads only the ones it needs.
Using context to skip one level of props
If a parent passes a value to its own child, that is one prop. It is not prop drilling. A context there hides where the value comes from. And the child quietly gets the default if no provider is above it. React’s docs: “Start by passing props.” Use context when many layers, or many far-away components, need the same value.
Practice
Press Edit on the examples above and try these. The playground uses Strict Mode, so count the Console lines carefully.
- In “Try this first”, add a third button with the label “Deeper”. Put it inside the
"dark"provider, wrapped in its own<ThemeContext value="blue">. What does it show? - In the “Context in three steps” example, remove the
<ThemeContext value="dark">tags soAppreturns only<Page />. What does the button say, and why? - In the “When the value changes” example, run it and click “Switch theme” twice. How many lines does the Console show in total? Which components do they name?
- In the object trap example (before the fix), click “Clicks” three times. How many “Profile renders” lines are there in total? Then try the same in the
useMemoversion.
Answers
- “Deeper: blue”. Its nearest provider is the
"blue"one. The page shows “Outside: light Inside: dark Deeper: blue”. - “Theme: light”. With no provider above
ThemeButton,useContextreturns the default value fromcreateContext('light'). - 16 lines. The first render gives 8:
Page,Toolbar,ThemeLabelandIcon, each twice because of Strict Mode. Each click adds 4:ThemeLabel renderstwice andIcon renderstwice.PageandToolbarnever log again. - 8 lines: 2 for the first render, and 2 for each of the 3 clicks, because each click gives the provider a new object. In the
useMemoversion there are only 2 lines, both from the first render.
Interview questions
Try to answer each one out loud before you open the answer.
What problem does context solve, and what should you try before it?
It solves prop drilling: passing a value down through many layers of components that don’t use it. With context, a provider high up gives a value, and any component below it can read it with useContext.
A strong answer gives React’s advice to try other things first. Pass props, even through several layers, because it makes the data flow clear. Or pass JSX as children, so fewer components sit between the data and the one that uses it. Context fits values that many far-away components need, like a theme, the current user or the current route.
When does a component get a context’s default value?
Only when there is no provider of that context anywhere above it. If any provider is above, its value wins, even if that value is undefined or null. A provider with no value prop gives undefined. In development, React 19.3 prints a warning that the value prop is required.
A strong answer says the default never changes. It is useful for tests, and for components that should still work with no provider. If a missing provider is always a bug, use a null default. Then add a custom hook that throws a clear error.
Two providers of the same context are above a component. Which value does it get?
The nearest one: the closest provider above it in the tree. This is how you change a value for one part of the page. A dark page can have a light menu.
A strong answer adds that useContext doesn’t see a provider that the same component returns. That provider is below the caller. And different contexts never replace each other’s values.
Does React.memo stop a component from rendering when a context it reads changes?
No. memo only compares props. When a context value changes, React renders every component that reads that context, wrapped in memo or not. React compares the old and new value with Object.is to decide if it changed.
A strong answer adds the other side. Some components sit between the provider and the readers and don’t read the context. Those can still be skipped, by memo or by passing them as children. A reader’s own children render too, because their parent rendered, unless they are wrapped in memo.
A provider’s value is an object written right in the JSX. Why can that cause extra renders? How do you fix it?
The braces make a new object on every render of the component that has the provider. Object.is sees a different object each time, so every reader renders, even when user didn’t change. In our test, Strict Mode was off. Three clicks that had nothing to do with the user rendered the reader three more times.
Fixes: wrap the object in useMemo with [user] as the dependency. Or split it into two contexts, one for user and one for setUser. Or keep the value a simple string or number. A strong answer knows that setUser from useState is already stable, so it doesn’t need useCallback. It also knows the fix pays off only if the components in between are skipped, for example by memo. Otherwise the readers render anyway, because their parents did. It may also mention the React Compiler. In our test with the compiler, the reader didn’t render on those three clicks.
How do you type a context in TypeScript when there is no good default?
Give the type with | null: createContext<Theme | null>(null). Then write a custom hook, like useTheme. It calls useContext and throws a clear error if the value is null. Otherwise it returns the value. After the check, TypeScript knows the value is a Theme, so components that use the hook need no null checks. React’s TypeScript page recommends this.
A strong answer adds that the error message should name the hook and the missing provider. Then the fix is obvious.
What are the two ways to write a provider? And how is use different from useContext?
<ThemeContext value> is the React 19 way to write a provider. <ThemeContext.Provider value> is the older form. It still works in React 19, and in React 19.3 it is the same object. The React team says it will deprecate .Provider in a future version.
useContext and use both read the nearest provider’s value. React’s docs call use “a React API”, not a Hook. An API is a name for something a library gives you to call. It can be called inside an if or a loop, where hooks are not allowed. It must still be called inside a component or a Hook, and not inside a try-catch block. It can also read a Promise, which is a separate topic.
Sources
- Passing Data Deeply with Context, react.dev: prop drilling, the three steps, nested providers, “Context is very tempting to use!”, trying props and
childrenfirst, and the use cases. - createContext, react.dev: the context object “does not hold any information”, the default value as a “last resort” that “never changes”, and
.Provideras “a legacy way”. - useContext, react.dev: the closest provider, overriding a part of the tree,
Object.is, memo “does not prevent the children receiving fresh context values”, the object trap and itsuseMemofix, and the missingvaluetroubleshooting. - use, react.dev:
useas “a React API”, reading context with it insideifand loops, and its rules (inside a component or a Hook, not inside try-catch). - useMemo, react.dev: React may throw a cached value away.
- React 19 blog post, react.dev:
<Context>as a provider, and the plan to deprecate<Context.Provider>. - Using TypeScript, react.dev: typing a context, and a
nulldefault with a hook that throws. - Scaling Up with Reducer and Context, react.dev: context with a reducer, named under “When to use context”. Part 24 builds on it.
- The render counts, the default-value cases, the warning text,
ThemeContext.Provider === ThemeContext, the TypeScript error and the thrown error above come from running React 19.3.0 and TypeScript 7.0.2 for this post. The compiler result comes frombabel-plugin-react-compiler1.0.0. The linter message comes from Oxlint 1.87.0 in the Part 10 project. - This part follows the Context API Deep Dive kata in react-katas.