Lifting state and Context, where does this data live?
Two components need the same state. Where does the state live? React's answer is older than hooks: it lives in the nearest common parent. That is "lifting state up".
When that parent is six levels above, you start passing props through components that do not care. That pain has a name: prop drilling. Context is the cure, but only sometimes.
Lifting state, the normal way
Two sibling components, one source of truth.
function App() {
const [city, setCity] = useState('Bengaluru');
return (
<>
<CitySelector value={city} onChange={setCity} />
<RestaurantList city={city} />
</>
);
}CitySelector writes. RestaurantList reads. The parent holds the state and passes it down. Boring, predictable, correct.
Prop drilling, the pain
Now RestaurantList lives five layers deep, inside <Page> > <Main> > <Feed> > <Section> > <List>. Every layer in between has to forward city even though it does not care.
- Three layers: annoying, fine.
- Five layers: time to think about it.
- Eight layers: you have an architecture smell, not just a prop problem.
Context, the lift you actually need
Context lets any descendant grab a value without props. Two halves: a provider that sets the value, a hook that reads it.
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Order</button>;
}Button reads theme without anybody passing it down. Beautiful when you mean it.
When Context is right
- Theme (dark or light).
- Current user / auth.
- Locale / language.
- App-wide feature flags.
Notice the pattern: things that almost everything reads, that almost never change.
When Context is overkill
- Form state. Use
react-hook-formor local state. - Server data. Use TanStack Query or SWR.
- Anything that changes 60 times a second. Animations, drag state.
- Three siblings sharing one value. Just lift to their parent.
Context is not a state manager. It is a way to pass a value through the tree. If you are reaching for Context to avoid lifting state twice, you are over-engineering.
The performance gotcha
Every consumer of a Context re-renders when the Context value changes. If you do this:
<UserContext.Provider value={{ user, setUser }}>You create a brand new object every render. Every consumer re-renders every time, even if user did not actually change. The fix:
- Memoize the value:
const value = useMemo(() => ({ user, setUser }), [user]); - Split contexts: one for
user(rarely changes), one forsetUser(never changes), one for transient stuff.
When to skip ahead to Zustand or Jotai
If you have multiple independent slices of global state, complex updates, or selector-style reads, a tiny store library (Zustand is the popular pick in 2026) saves you from Context gymnastics. Reach for it when Context feels like the wrong shape, not before.
Next: fetching data, which is most of what real apps actually do.
Free tools you can use while you learn
Watching quietly. Tap me if you want a tip.
Try this (0 of 1 done)
- 1
Add a "clear" button that sets the text back to empty.
show answer
function App() { const [text, setText] = React.useState(''); return ( <div> <Input value={text} onChange={setText} /> <Display value={text} /> <button onClick={() => setText('')}>clear</button> </div> ); }