Component frameworks (React, Vue, Svelte) revolve around one question: where does the data live and who can change it? Getting this right is the difference between a predictable UI and a tangle of bugs where nothing is in sync. State and props are the two sides of that coin.
Props: the immutable input
Props are the data a component receives from whoever uses it. From the component's perspective, they are immutable: it reads them but doesn't change them. Think of a cake recipe — the ingredients come from outside, and the recipe can't rewrite the shopping list; it can only use what arrived.
State: the mutable internal data
State is the data a component owns and can change over time: an input's text, whether a modal is open, a counter. When state changes, the framework re-renders the component to reflect the new value.
function Contador() {
const [n, setN] = useState(0);
return (
<button onClick={() => setN(n + 1)}>
Cliquei {n}x
</button>
);
}
| Props | State | |
|---|---|---|
| Origin | Parent component | The component itself |
| Mutable? | No (read-only) | Yes (via setter) |
| Who controls it | Whoever renders it | The component |
Unidirectional data flow
Data flows down from parent to child via props; events flow up via callbacks. This unidirectional flow is what makes the UI predictable: to find out why something is on screen, you follow the data from top to bottom, in a single direction. It's like a waterfall — the water only goes down.
Lifting state up
And when two sibling components need the same data? You don't duplicate the state in each — that causes desynchronization. You lift the state up to the nearest common ancestor, which then passes the value down via props. The data lives in a single place: the "source of truth".
function Pai() {
const [texto, setTexto] = useState('');
return (
<>
<Input valor={texto} aoMudar={setTexto} />
<Preview valor={texto} />
</>
);
}
Derived state and controlled inputs
Beware of derived state: if a value can be computed from another piece of state, don't store it separately — compute it on the fly. Storing the list and also "how many items" creates two sources of truth that will diverge.
A controlled input is one whose value comes from state: what appears on screen is always what's in state, and typing fires an event that updates the state. This gives you full control — validating, formatting, clearing — because React owns the value, not the DOM.
The rule that prevents 90% of state bugs: each piece of data has a single source of truth. If the same value exists in two places, they will drift apart.
When to reach for global state
Lifting state works until you catch yourself passing the same prop through five levels ("prop drilling") just to reach the bottom. That's where global state comes in (Context, Redux, Zustand, signals): data that many distant parts of the tree consume — logged-in user, theme, cart. But don't start there: global state has a complexity cost. Use it when local state truly doesn't scale.
Interviews commonly ask you to explain the difference between props and state, or to describe when you'd lift state up. Day to day, it shows up every time two components "should show the same number" and don't.
Conclusion
Props come in and don't change; state is internal and changes; data flows down and events flow up. Lift state when siblings share data, avoid duplicating what can be derived and only reach for global state when prop drilling hurts. With a predictable UI, the next focus is making it render fast.