Props vs State in React: What Is the Difference?
Advertisement
🧩 Scenario
Architecture Walkthrough
Props: External Inputs
Props are data passed from a parent component to a child component. They are immutable from the child's perspective: a component receives props and uses them to render, but must never modify them. This constraint is what makes React's data flow predictable. The single source of truth for any prop value is the parent, and the parent controls when and how that value changes.
When a parent re-renders (because its own state changed or its parent's props changed), it passes potentially new prop values to its children. React compares the new props with the previous ones during reconciliation and re-renders children that received changed props.
State: Internal Mutable Data
State is data owned and managed by a component itself. It represents things that can change over time in response to user interaction, network responses, or timers. useState returns the current value and a setter function. Calling the setter with a new value schedules a re-render with the updated state.
State is local and encapsulated by default. A parent component cannot read a child's state (unless the child explicitly exposes it via a callback or a ref). When two components need to share state, the state must be lifted to their closest common ancestor, which then passes the value down as props to both children.
One-Way Data Flow
React enforces a one-way data flow: props flow down the component tree from parent to child, and events (callbacks passed as props) flow up. A child that needs to change data owned by its parent does so by calling a callback prop (e.g., onChange, onSubmit). The parent updates its state in response and passes the new value back down as a prop. This creates a clear, traceable cycle.
Key Code Explained
// Parent owns state, passes it down as props with a callback for changes
function Counter() {
const [count, setCount] = useState(0); // state: internal, mutable
return (
<div>
{/* Passing state down as a prop and a callback up */}
<Display count={count} />
<Controls
onIncrement={() => setCount((c) => c + 1)}
onDecrement={() => setCount((c) => c - 1)}
/>
</div>
);
}
// Display: receives count as a prop, read-only
function Display({ count }: { count: number }) {
// NEVER: count = count + 1 ← mutating a prop, prohibited
return <p>Current count: {count}</p>;
}
// Controls: receives callbacks as props
function Controls({
onIncrement,
onDecrement,
}: {
onIncrement: () => void;
onDecrement: () => void;
}) {
return (
<div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
);
}
// Danger: initializing state from a prop
function SearchInput({ initialQuery }: { initialQuery: string }) {
// Bug: if the parent passes a new initialQuery, this state does NOT update.
// useState only uses its argument for the FIRST render.
const [query, setQuery] = useState(initialQuery);
return (
<input value={query} onChange={(e) => setQuery(e.target.value)} />
);
}
// Fix option 1: Use the prop directly (no state needed if parent controls the value)
function SearchInput({ query, onChange }: { query: string; onChange: (v: string) => void }) {
return <input value={query} onChange={(e) => onChange(e.target.value)} />;
}
// Fix option 2: Sync with useEffect if you need local state AND prop updates
function SearchInput({ initialQuery }: { initialQuery: string }) {
const [query, setQuery] = useState(initialQuery);
useEffect(() => {
setQuery(initialQuery); // re-sync when prop changes
}, [initialQuery]);
return (
<input value={query} onChange={(e) => setQuery(e.target.value)} />
);
}
// Mutation bug: why object/array mutation does not trigger re-render
function ItemList() {
const [items, setItems] = useState(['a', 'b', 'c']);
const addItem = () => {
// BUG: mutating the existing array, React sees the same reference
items.push('d');
setItems(items); // same array reference, no re-render
// FIX: create a new array
setItems([...items, 'd']); // new reference, re-render triggered
};
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
The setter function from useState accepts a value or an updater function. Using the updater form (setCount(c => c + 1)) is important when the new state depends on the previous state. If two state updates are batched in the same event handler and both use the raw value form (setCount(count + 1) twice), both reads see the same stale count value and only one update takes effect. The updater form always receives the latest queued state.
Tradeoffs
| Data type | Who owns it | Who can mutate it | Triggers re-render when |
|---|---|---|---|
| Props | Parent | Only the parent | Parent re-renders with new value |
| State | The component | Only the component | Setter called with new value |
What Interviewers Actually Check
- Whether you know props are immutable from the child's perspective
- Whether you know that calling the state setter with the same reference does not trigger a re-render
- Whether you can explain the parent-state-to-child-prop pattern and how callbacks flow back up
- Whether you know the danger of initializing state from a prop
- Whether you can explain why state must be lifted when two siblings need to share data
Follow-Up Questions
- What is the difference between controlled and uncontrolled components in terms of state and props?
- How does
useReducercompare touseStatefor managing complex state with multiple sub-values? - If two deeply nested components need the same state, what are the alternatives to prop drilling?
- What is the updater function form of
useState(setState(prev => newValue)) and when is it required? - Why does React use immutability for state updates and how does this enable efficient reconciliation?
Common Candidate Mistakes
- Saying props can be modified inside a child component (they cannot)
- Mutating an array or object in state and calling the setter with the same reference, wondering why no re-render happens
- Initializing state from a prop and not knowing that subsequent prop changes do not automatically update the state
- Not understanding the updater function form and producing stale state bugs in event handlers that fire multiple updates
- Lifting state too high unnecessarily, causing unrelated sibling components to re-render on every state change
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain the one-way data flow: props flow down, callbacks flow up?
- Can you describe why mutating a prop is prohibited?
- Can you describe what triggers a re-render: state setter call with a new value, or fresh props from parent?
- Can you describe the danger of initializing state from a prop and how to fix it?
- Can you explain why object mutation without a new reference does not trigger a re-render?
Summary
Props are the inputs to a component: values passed from parent to child. A component reads its props and renders from them but never modifies them. This constraint gives React's data flow its directionality: data flows down and events (callbacks) flow up. When a parent changes its state, it re-renders and passes new prop values to its children.
State is data a component owns internally. useState provides the current value and a setter. Calling the setter with a new value schedules a re-render. The key constraint is immutability: React detects state changes by reference comparison. Mutating an object or array in place and calling the setter with the same reference does not trigger a re-render. Always replace state with a new value or reference.
When two components need to share state, the state belongs in their closest common ancestor, which passes the value down to both as props. This lifting pattern, combined with callbacks for updates, keeps data ownership explicit and makes the flow of data traceable through the component tree.
Can a component pass its state to a child as props?
Yes. This is the standard pattern. A parent holds state with useState, then passes the state value down to a child as a prop. The child renders from the prop but never owns or mutates the state directly.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement