Explain why React treats props as read-only inputs (UI = f(props, state, context)). Connect this to predictable rendering, parent ownership, referential equality optimizations (PureComponent/React.memo), and why mutating props can cause missed updates, shared-state bugs, and inconsistent UI—especially under concurrent rendering and StrictMode.
Why are props immutable in React, and what breaks if they aren’t?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Props are inputs owned by the parent. React’s mental model is: UI = f(props, state, context). If a child can mutate its inputs, rendering stops being a pure computation and you get hidden side effects and shared mutable state across components.
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.