Context
Lifting state up works well when the components sharing a value sit close together. It gets painful when they don't. A value that lives near the top of the tree and is needed six levels down has to be passed through every component in between, and each of those components has to accept and forward a prop it never uses. That relay is prop drilling, the problem Compound components ran into and could not solve with Children.map. Context is React's built-in way out of it: one component provides a value, and any component beneath it can read that value directly, no matter how many layers sit in between.
Think of it as teleporting the data. The components in the middle never see it and never forward it; their prop lists stay about their own jobs.
Creating a context and providing a value
Context has three parts: create it, provide a value, read the value. Creating it happens once, at the top level of a file, outside any component. You'll usually export it too, because the components that read it often live in other files.
import { createContext } from 'react'
export const ThemeContext = createContext()
export default function App() {
return (
<ThemeContext value="light">
<Header />
<Button />
</ThemeContext>
)
}Rendering the context object itself as a component makes everything inside it a consumer-in-waiting. The value prop is the payload: here it's the string "light", but it can be any JavaScript value, an object, an array, even a function. Whatever you put there is what readers below will receive. The prop must be spelled value; that name is part of the API.
Placement matters. The provider doesn't have to wrap your whole app, and usually it shouldn't. Put it around the smallest subtree that covers every component needing the value. If only one corner of the UI cares about the theme, provide the theme at that corner's common ancestor and leave the rest of the tree out of it.
Version note
Before React 19 a context couldn't be rendered directly; you rendered <ThemeContext.Provider value="light"> instead. React 19 made the context object itself renderable as the provider. The .Provider form still works and is what you'll see in most existing codebases and in the course lessons, but React plans to deprecate it in a future version and ships a codemod for the switch, so reach for the bare <ThemeContext> form in new code.
Reading the value with useContext
Any component under the provider reads the value with useContext, the hook first introduced in Hooks, passing it the context object so React knows which context to read. That's why the context gets exported.
import { useContext } from 'react'
import { ThemeContext } from './App'
function Header() {
const theme = useContext(ThemeContext)
return (
<header className={`${theme}-theme`}>
<h1>{theme === 'light' ? 'Light' : 'Dark'} Theme</h1>
</header>
)
}useContext(ThemeContext) returns whatever the nearest ThemeContext provider above this component is currently holding: here, the string "light". There's no prop threading between App and Header, and components between them in a larger tree wouldn't mention the theme at all. An app can have several contexts at once, each created separately and each read by passing its own object to useContext.
Making it live: state plus context
A hard-coded value="light" never changes. The pattern that makes context useful pairs it with state: state owns the value and updates it, context delivers it. Pass an object holding both the current value and a function that changes it, and every consumer can read or update the theme from anywhere below the provider.
export const ThemeContext = createContext()
export default function App() {
const [theme, setTheme] = useState('light')
function toggleTheme() {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
<Header />
<Button />
</ThemeContext>
)
}
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button className={`${theme}-theme`} onClick={toggleTheme}>
Switch theme
</button>
)
}When the button is clicked, toggleTheme runs, the state in App updates, App re-renders, and the provider passes the new object down. Every component reading the context re-renders with the fresh value. Consumers destructure only the properties they need.
This scoping trick also works in the small. A component like a dropdown menu can render a provider around its own children, giving the pieces inside it a shared state that never leaks to the rest of the page. Here is the menu from Compound components, finished:
const MenuContext = createContext()
function Menu({ children }) {
const [open, setOpen] = useState(false)
const toggle = () => setOpen(prevOpen => !prevOpen)
return <MenuContext value={{ open, toggle }}>{children}</MenuContext>
}
function MenuButton({ children }) {
const { toggle } = useContext(MenuContext)
return <button onClick={toggle}>{children}</button>
}Menu owns open and puts both it and toggle on the context; MenuButton reaches up for the toggle with useContext, and a MenuDropdown written the same way reads open to decide whether to render. Nothing in between relays a thing, so a caller can wrap the button in as many layout divs as they like and the menu keeps working.
Context lets one component near the top say "here is the value" and lets any component below reach up and grab it directly with useContext. The components in the middle never touch it.
Pair it with state and the value can change too: keep the value and its update function together in the provider, and any consumer can read or change it.
Next up: Render props and headless components, where a component provides behavior and lets you supply the markup.

