Explain props immutability as a production contract: parent ownership, predictable rendering, memoization safety, and the shared-state bugs you debug when children mutate props.
Why are props immutable in React, and what breaks if they aren’t?
Core idea
Props are inputs owned by the parent. React’s mental model is: UI = f(props, state, context). If a child mutates those inputs, rendering stops being a pure computation and you get hidden side effects, broken memoization assumptions, and shared mutable state bugs that are painful to debug in production.
Why props are read-only | What React assumes | What you gain |
|---|---|---|
Parent owns the data | Only the parent decides when/why props change | Clear ownership + easier reasoning |
Render should be pure | Same inputs => same output | Safe re-renders, retries, and dev StrictMode stress tests |
Optimizations rely on identity | Shallow comparisons (memo/PureComponent) are meaningful | Skipping work becomes correct and predictable |
What breaks if you mutate props
JavaScript won’t stop you from mutating objects/arrays passed via props, but React will still treat props as if they were immutable. That mismatch causes bugs.
Breakage | What it looks like | Why it happens |
|---|---|---|
Missed re-renders / stale UI | You change a field, but UI doesn’t update (or updates inconsistently) | Parent didn’t produce a new reference; memo/shallow compare thinks “nothing changed” |
Shared mutable state bugs | One child “mysteriously” affects another sibling | They both reference the same mutated object/array |
Invalid assumptions in concurrent rendering | Weird flickers, order-dependent bugs, “it depends on timing” | React may render, pause, retry; mutations during render leak across attempts |
Debugging becomes painful | You can’t trace where the data changed | Mutation hides the real source of change (no single writer) |
import React from 'react';
const UserCard = React.memo(function UserCard({ user }) {
// ❌ Mutating a prop object
user.lastSeenAt = Date.now();
return (
<div>
<div>{user.name}</div>
<div>lastSeenAt: {user.lastSeenAt}</div>
</div>
);
});
export default function App() {
const [user, setUser] = React.useState({ id: 1, name: 'Ada', lastSeenAt: 0 });
// Parent does NOT create a new object unless setUser is called.
// UserCard is memoized; shallow compare sees same `user` reference and may skip re-render.
return (
<>
<UserCard user={user} />
<button onClick={() => setUser((u) => ({ ...u, name: u.name + '!' }))}>
Update name (new object)
</button>
</>
);
}
Another classic footgun: mutating arrays
Sorting, pushing, splicing, or reversing a prop array mutates in-place and can corrupt parent/sibling views.
function List({ items }) {
// ❌ Mutates parent-owned array in-place
items.sort((a, b) => a.label.localeCompare(b.label));
return <ul>{items.map((x) => <li key={x.id}>{x.label}</li>)}</ul>;
}
// ✅ Make a copy
function ListSafe({ items }) {
const sorted = [...items].sort((a, b) => a.label.localeCompare(b.label));
return <ul>{sorted.map((x) => <li key={x.id}>{x.label}</li>)}</ul>;
}
If you need to “change props”… | Do this instead |
|---|---|
Child needs to request an update | Pass a callback prop (events up), parent updates state and passes new props down |
Need derived data | Compute immutably (copy + transform) inside render or memoize with useMemo if expensive |
Need local editable state | Initialize local state from props carefully (and handle updates explicitly) |
Props are immutable because React’s rendering model assumes components are pure functions of their inputs, and because performance optimizations depend on stable identity + parent ownership. If you mutate props, you introduce hidden side effects: missed updates, cross-component contamination, and timing-sensitive bugs—especially with memoization and concurrent rendering.
Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.