Imagine a shopping website that has a cart counter icon in the navbar, on the checkout button, and in the mini-cart dropdown. Now imagine that every time you add another item to the cart, you'd have to go update the counter manually in every single one of those places to keep them all in sync.
Luckily, this is a problem that was solved about 13 years ago by developers at Meta. They built a JavaScript framework, one that numerous platforms have since been built on top of, called React.
In React, the UI you see is simply a function of the data behind it. Whenever that data changes, the component re-renders and displays the updated UI shaped by the new data. React works with two types of data: props and state.
Props
A prop is like a topping someone else ordered for your pizza. It's data passed down from a parent component to a child. Just like the toppings, the child has no say over it; it just receives it and displays it. This is what makes props read-only, or immutable, from the child's perspective. Props aren't limited to plain data either. A prop can also be a function.
This read-only rule is what makes props useful for tracing where your UI comes from. If you want to change what's rendered, you know exactly where to look: up, at the parent that owns the data.
Here's props in action:
function Parent() {
return <Child name="Sarah" age={10} />;
}
function Child({ name, age }) {
return (
<div>
<h1>My name is {name}</h1>
<p>I am {age} years old</p>
</div>
);
}
Parent owns the data and hands it down. Child receives it as props and just displays it. It never touches or changes it directly.
State
The other data type, the one that works alongside props, is state. State is data owned by a particular component: the component itself can change it, and changing it triggers a re-render so the UI reflects the new value. State comes paired with a setter function for updating it.
Here's a simple example:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
You clicked {count} times
</button>
</div>
);
}
Who decides when this value changes?
If it's a parent component, it's a prop.
If it's the component itself, it's state.
The same value can be both, depending on where you're standing
Props and state aren't two permanent categories that data gets sorted into. They're roles, and the exact same value can play a different role depending on which component you're looking at.
Take a simple name input:
function NameInput() {
const [name, setName] = useState("");
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
From inside NameInput, name is state. It owns it. It updates it every time someone types a character.
Now say the parent form wants to show a live preview of that name elsewhere on the page:
function Form() {
const [name, setName] = useState("");
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<Preview name={name} />
</>
);
}
function Preview({ name }) {
return <p>Hello, {name}!</p>;
}
Look at Preview. That exact same value, the one that started as state inside NameInput, is now sitting inside Preview as a prop. Preview didn't create it. It can't change it. It just receives it and renders it. It has no idea that somewhere upstream, this value began life as someone's state.
Same data. Two different identities, depending entirely on which component you're standing in when you ask the question.
This is why "props and state are two kinds of data" is a slightly misleading way to think about it. The better framing: state is what you call data at the place it's owned, and props are what you call that same data everywhere it's received. A value isn't permanently one or the other. It just changes identity as it crosses a component boundary.
It's also why the test from earlier, who decides when this changes, is one you have to re-run per component, not once per value. Ask it inside NameInput, and the answer is "the input itself." Ask it inside Preview, and the answer is "whoever rendered me." Same question, same value, different answer, because the vantage point changed.
When two components need to share the same state
If state belongs to one component only, what happens when two components both need access to the same value?
Say you have the Child component from earlier, but now imagine you want a SummaryBadge next to it that also needs to show age. Your first instinct might be to let Child hold its own state:
function Child() {
const [age, setAge] = useState(10);
return <p>I am {age} years old</p>;
}
This works fine, right up until SummaryBadge also needs that number. And here's the problem: age is sealed inside Child. Nothing outside Child, not a sibling, not even Parent, can reach in and read it. It's a private notebook that only Child is allowed to open.
The fix isn't clever, it's just relocation: move the state to whichever component is the closest shared parent of everyone who needs it.
function Parent() {
const [age, setAge] = useState(10);
return (
<>
<Child age={age} onBirthday={() => setAge(age + 1)} />
<SummaryBadge age={age} />
</>
);
}
function Child({ age, onBirthday }) {
return <button onClick={onBirthday}>I am {age}, Happy Birthday</button>;
}
function SummaryBadge({ age }) {
return <span>Current age: {age}</span>;
}
Now Parent owns age. It passes the value down to both Child and SummaryBadge as props, so they stay in sync automatically. Notice what it passes to Child: not the state itself, but a function, onBirthday, that calls setAge on Child's behalf. Child never touches useState. It just calls the function it was handed.
This pattern is called lifting state up, and the reasoning behind it isn't about style. It's about access. Ownership determines visibility. Whoever owns a piece of state is the only one who can read or change it directly, so the moment more than one component needs to see the same value, it has to live in whichever component is the nearest shared parent of everyone who needs it.
The rule of thumb
Whenever you're unsure what a piece of data should be, ask yourself: does this value need to be seen by more than one component?
If yes, it doesn't live where you first wrote it. It lives in their nearest shared parent.
Top comments (0)