Lifting state up works well when the components that need to share data are close together. It breaks down when a value — the current theme, the logged-in user, the selected language — is needed by components scattered deep across the tree, with layers of unrelated components in between that would have to pass it through for no reason of their own.
function App() {
const [theme, setTheme] = useState('light');
return <Page theme={theme} />;
}
function Page({ theme }) {
return <Sidebar theme={theme} />; // Page never actually uses theme itself
}
function Sidebar({ theme }) {
return <Avatar theme={theme} />; // neither does Sidebar
}
function Avatar({ theme }) {
return <img className={theme} />; // only this one actually needs it
}This chain — passing a prop through components that don't use it themselves, purely to reach one that does — is called prop drilling. It works, but it makes every component in the middle depend on something that isn't really its concern.
import { createContext, useState, useContext } from 'react';
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}Any component anywhere inside the Provider can read the value directly — no props passed through the components in between at all:
function Page() {
return <Sidebar />; // no theme prop needed here anymore
}
function Sidebar() {
return <Avatar />; // or here
}
function Avatar() {
const theme = useContext(ThemeContext); // reads it directly
return <img className={theme} />;
}Context is the right tool for genuinely global, rarely-changing values — theme, logged-in user, current locale. For state that's only shared between two or three closely related components, lifting state up (the earlier lesson) is usually simpler and keeps the data flow easier to trace. Reaching for context by default, everywhere, trades one problem (prop drilling) for another (hard-to-trace implicit dependencies).