Props vs state in React
Props are inputs passed in from a parent — read-only inside the component. State is internal, mutable via `setState`. Props flow down; events flow up. If multiple components need the same value, lift it to the lowest common parent and pass it down as a prop. If a piece of data can be derived from props, don't put it in state.
Two ways data lives in a React component.
Props. Input from the parent. Read-only.
function Avatar({ user, size }: { user: User; size: number }) {
// user and size are props — set by the caller, can't be reassigned here
return <img src={user.avatarUrl} width={size} />;
}State. Internal, owned by the component, mutated via a setter.
function Counter() {
const [count, setCount] = useState(0); // state
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Data-flow rules.
- Props flow down. Parent → child, never the reverse.
- Events flow up. Children call callbacks passed down as props; the parent decides what to do with them.
- State lives at the lowest common ancestor of the components that need it. This is called lifting state.
// Two siblings need the same temperature value.
function App() {
const [temp, setTemp] = useState(0);
return (
<>
<Slider value={temp} onChange={setTemp} />
<Display value={temp} />
</>
);
}temp lives in the parent because both children need it. The slider takes it as a prop and reports changes via a callback prop.
The two rules that matter most.
- Don't put derived data in state.
```tsx // Bad — derived state. fullName drifts out of sync. const [first, setFirst] = useState(""); const [last, setLast] = useState(""); const [fullName, setFullName] = useState(""); // ← don't
// Good — compute on render. const fullName = ${first} ${last}; ```
If you can compute it, do — every render is cheap, and you eliminate a class of "I forgot to update X when Y changed" bugs.
- Don't copy props into state without a reason.
``tsx // Usually wrong function Field({ value }) { const [internal, setInternal] = useState(value); ... } ``
The component now ignores future changes to value from the parent. Use it directly. Copy into state only when you genuinely want to fork the value (e.g., a draft for editing that the user can cancel).
When you DO want to mirror a prop into state.
The "uncontrolled-with-controlled-reset" pattern:
<Editor key={item.id} initialValue={item.text} />Inside, useState(initialValue) is fine, because key={item.id} forces a remount when the parent's selected item changes — so internal state always starts from the right prop.
Props can be functions, objects, JSX. Anything. Children-as-prop is common:
<Card title="Hi">
<p>body</p>
</Card>
// Inside Card, you receive a `children` prop with the <p> element.Context bypasses the prop chain — but it's not state. It's a way to deliver a value (which itself is state somewhere up the tree) to deeply nested consumers without manual prop drilling.
Senior framing. The mental model is: state is the minimum amount of mutable input the component needs to remember between renders. Everything else is either a prop (passed in) or a derived value (computed during render). Most "why is my component buggy" questions trace back to having too much state — variables that should have been derived.
Follow-up questions
- •When should you lift state vs use context?
- •Why is derived state a common bug source?
- •When is it correct to mirror a prop into state?
- •What does 'data flows down, events flow up' mean in practice?
Common mistakes
- •Putting derived values (sum, fullName, filtered list) in state.
- •Copying props into state unnecessarily, so prop updates are ignored.
- •Mutating a prop or an object inside state directly.
- •Adding state in too many places instead of lifting once.
Performance considerations
- •Each setState triggers a re-render of the owning component and its subtree.
- •Splitting state into multiple useState calls is fine; React batches updates in event handlers.
Edge cases
- •Async setState calls inside async functions in React 17 don't auto-batch (fixed in 18).
- •Updating state during render causes a loop unless guarded.
- •Storing functions in state — useState lazily calls them; pass via setter callback instead.
Real-world examples
- •Form input value (controlled) — state. Form schema — prop. Theme — context (state higher up).