Props vs State
Props are inputs from the parent; state is data the component owns over time. How to choose, lift, and avoid the usual traps.
- react
- props
- state
Every React UI is a function of props and state (plus context). Mixing them up produces the classic bugs: “I updated the prop and nothing happened,” “two sources of truth,” and “why does typing reset when the parent re-renders?”
| Props | State | |
|---|---|---|
| Who owns it? | Parent (or caller of the component) | The component that declared useState / useReducer |
| Can the child change it? | No — props are read-only inputs | Yes — via the setter |
| When does it change? | Parent re-renders with new values | After a state update is processed |
| Mental model | Function arguments | Memory over time for this instance |
Docs: Passing Props, State: A Component’s Memory.
Props: the public API of a component
function Avatar({ src, alt, size = 40 }) {
return (
<img
src={src}
alt={alt}
width={size}
height={size}
className="rounded-full"
/>
);
}
// Parent decides values
<Avatar src={user.avatarUrl} alt={user.name} size={48} />
Props make a component reusable. The same Avatar renders different people without knowing where the data lives.
Conventions that scale:
- Treat props as immutable from the child’s point of view. Don’t mutate
props.user.name = …. - Prefer explicit props over a giant
configbag until you have a real pattern. - Use
childrenfor composition slots:
function Panel({ title, children }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
State: memory for one component instance
function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((c) => c + 1)}>
Clicked {count}
</button>
);
}
State updates are asynchronous from your perspective (scheduled). Reading count immediately after setCount still shows the old value in the same event handler — use the next render or a functional update when the new value depends on the previous one.
State is tied to the component identity in the tree (type + position + key). Unmount and remount → state resets. Change key intentionally when you want a clean slate (e.g. key={userId} on a form).
Choosing: does this component own the data?
Ask:
- Can the parent compute it? → prop or derive during render
- Does only this component care over time (open/closed, draft text)? → local state
- Do siblings need the same data? → lift state to the closest common parent
- Does half the app need it? → context or external store — not every global is “context”
Controlled vs uncontrolled inputs
Controlled: parent/state is the source of truth for the value.
function Search({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
aria-label="Search"
/>
);
}
Uncontrolled: the DOM holds the value; you read via ref when needed.
function FilePicker() {
const inputRef = useRef(null);
function onSubmit() {
const file = inputRef.current?.files?.[0];
// ...
}
return <input ref={inputRef} type="file" />;
}
Pick one model per field. The hybrid “state mirrors props without a reset strategy” is how forms desync.
Lifting state (and putting it back down)
function FilterableList({ items }) {
const [query, setQuery] = useState('');
const visible = items.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<Search value={query} onChange={setQuery} />
<ul>
{visible.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
);
}
query lives in the parent because both the input and the list need it. Search stays dumb and testable.
Anti-pattern: duplicate state in parent and child that can diverge.
// ❌ child copies prop into state once and never follows updates
function Bad({ color }) {
const [c, setC] = useState(color);
return <div style={{ background: c }} />;
}
If the prop is the source of truth, use the prop. If you need a draft that starts from a prop, document the reset rule (key, or sync intentionally when an id changes).
Derived values are not state
// ❌
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((s, i) => s + i.price, 0));
}, [items]);
// ✅
const total = items.reduce((s, i) => s + i.price, 0);
Extra state for pure calculations causes extra renders and stale windows. Same idea as in useEffect Fundamentals: don’t sync what you can compute.
Props drilling vs context
Drilling 2–3 levels is fine. When every intermediate component only forwards props, consider context for stable ambient data (theme, locale, auth user) — not for high-frequency values like mouse position unless you’ve measured and designed for it.
Interview angle
Q: Difference between props and state?
A: Props are external inputs controlled by the parent; state is internal memory. Children should not mutate props. Updates to either can trigger re-render.
Q: When do you lift state?
A: When multiple children need the same changing data, or the parent must coordinate.
Q: Controlled component?
A: Form element whose value is driven by React state/props every render, with onChange updating that state.
Bonus: explain that re-render ≠ DOM update (Reconciliation) and that identity/key affects whether state is preserved.
Related on this site
- Rules of Hooks
- useEffect Fundamentals
- Reconciliation
- Keys in Lists
- Todo App (machine coding) — props/state in a full UI
Further reading
- State: A Component’s Memory
- Sharing State Between Components
- Choosing the State Structure
- Reacting to Input with State
Related guides
- Context API BasicsReact context for dependency injection — createContext, Provider, useContext, default values, and when context is the wrong tool.
- useReducer for Complex StateWhen useReducer beats useState — action tables, pure reducers, dispatch stability, context pairing, and migration patterns.
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.