React State vs Props vs Context: Who Should Own the Data?
A simple rule for where data lives in React: local state, props, lifted state or Context, plus prop drilling fixes and when to use a store.
Every React app eventually hits the same question: this piece of data, where should it live? In the component that shows it? In its parent? In Context? In a global store?
Get it wrong and you'll either pass a prop through six layers of components, or wrap everything in Context and wonder why the whole page re-renders when someone types.
Here's a simple way to decide.
The simple idea: who owns the ball?
In a football match, only one player has the ball at a time. Others can see it and call for it, but only the player with the ball decides what happens next.
In React:
- State is the ball. One component owns it and is the only one allowed to change it.
- Props are what the other players see: read-only information passed down.
- Context is the stadium screen: everyone in the stadium can see it without anyone passing it along.
The key question is always: who needs to change this data, and who only needs to read it?
State: data a component owns
import { useState } from "react";
function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>;
}Only LikeButton cares whether it's liked, so it owns the state. This is the default: keep state as close as possible to where it's used.
Props: data passed down, read-only
function ProductCard({ name, price }) {
return (
<article>
<h3>{name}</h3>
<p>₹{price}</p>
</article>
);
}
<ProductCard name="Desk lamp" price={1250} />ProductCard receives name and price but never changes them. If it tried (price = 999), it would only change its local copy, and React would overwrite it on the next render.
A useful way to see it: the same value can be state in one component and a prop in another. The owner has it as state; everyone it's passed to receives it as a prop.
Lifting state up: when two components need the same data
A filter input and a product list both need the search text. The input changes it; the list reads it. Neither can own it alone, so move it to their closest common parent:
function Shop({ products }) {
const [query, setQuery] = useState("");
const visible = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<SearchBox query={query} onChange={setQuery} />
<ProductList products={visible} />
</>
);
}
function SearchBox({ query, onChange }) {
return <input value={query} onChange={(e) => onChange(e.target.value)} />;
}Shop owns query. SearchBox gets the value and a function to request changes. ProductList just gets the result. Data flows down; change requests flow up through callbacks.
Notice visible isn't state. It's calculated from products and query on every render. Don't store what you can calculate. Two copies of the same truth eventually disagree.
Prop drilling: when passing down gets silly
Now the logged-in user's name is needed in the header's avatar, three layers down:
<App user={user}>
<Layout user={user}>
<Header user={user}>
<Avatar user={user} />Layout and Header don't use user; they just carry it. That's prop drilling. A little is fine and even helpful, because you can see exactly where data comes from. When it spans many layers, there are two fixes.
Fix 1: composition (try this first)
Pass the finished component instead of the data:
<Layout header={<Header avatar={<Avatar user={user} />} />} />Layout now receives a ready-made header and doesn't need to know about user at all. This solves more prop drilling than people expect.
Fix 2: Context
For data that truly many components need, at many depths, use Context:
import { createContext, useContext, useState } from "react";
const UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used inside UserProvider");
return ctx;
}
// anywhere below the provider:
function Avatar() {
const { user } = useUser();
return <img src={user.avatarUrl} alt={user.name} />;
}The custom useUser hook gives a clear error if someone forgets the provider, instead of a mysterious null.
(In React 19 you can also write <UserContext value={...}> without .Provider. Both work.)
The Context catch: everyone re-renders
When a Context value changes, every component that reads that Context re-renders. For the current user or theme, which change rarely, that's no problem. For something that changes on every keystroke, like a search query, wrapping the app in that Context makes the whole page re-render as the user types.
Also, value={{ user, setUser }} creates a new object each render of the provider. If the provider re-renders often, wrap the value in useMemo.
So Context is great for widely read, rarely changed data: user, theme, language, feature flags. It isn't a general-purpose state manager.
The decision guide
| The data is... | Put it in |
|---|---|
| Used by one component | Local state |
| Needed by a parent and a child | State in the parent, prop to the child |
| Shared by siblings | Lift state to their common parent |
| Calculable from other state or props | Nowhere: calculate it during render |
| Read in many places, changes rarely (user, theme, language) | Context |
| Shared widely and updated often (cart, editor, complex UI) | A store such as Zustand or Redux Toolkit |
| Fetched from a server | A data library such as TanStack Query (see fetching data safely) |
| Something that should survive a refresh or be shareable | The URL (search params) |
What goes wrong
- Copying props into state.
useState(props.price)only reads the prop once. When the parent sends a new price, your copy is stale. Use the prop directly. - Global state for everything. Putting every value in Context or a store makes components hard to reuse and every change ripple everywhere.
- Two sources of truth. Storing both
itemsanditemCountin state means one day they'll disagree. Storeitems, calculate the count.
For the related bug where callbacks see old state, read JavaScript closures with React examples.
In one line: keep state in the lowest component that needs to change it, pass it down as props, lift it when siblings share it, and use Context only for data many components read but rarely change.
Keep reading
- Why It's Time to Move Your Website Off WordPress (and Why Next.js Wins for Static Sites)Frontend Development, 11 min read
- React useEffect Explained: Dependencies, Cleanup and Infinite LoopsFrontend Development, 5 min read
- React Controlled vs Uncontrolled Components ExplainedFrontend Development, 5 min read
New games, tools and tutorials
About one short email a week. No spam, one-click unsubscribe.