Props Are Read-Only
One of React's core rules is simple but non-negotiable: a component must never modify its own props. Props are read-only. They represent a snapshot of data delivered from the parent at the moment of rendering, and the child has no authority to change them.
The Pure Function Analogy
React components are expected to behave like pure functions with respect to their props. A pure function:
Returns the same output given the same inputs
Does not modify its input arguments
Has no side effects that affect the outside world
Consider this mathematical function: f(x) = x * 2. No matter how many times you call it with x = 5 it always returns 10, and it never changes the value stored in x. React components should work the same way with respect to their props.
// Pure function — same input always gives the same output, nothing mutated
function double(x) {
return x * 2
}
// Impure function — mutates its argument
function addTax(order) {
order.total = order.total * 1.1 // ✗ mutates the input
return order
}What Happens When You Mutate Props
// ✗ WRONG — mutating the props object
function DiscountBadge({ product }) {
// This appears to work locally, but it corrupts the parent's data
product.price = product.price * 0.9 // ✗ never do this
return <span>Sale: ${product.price.toFixed(2)}</span>
}
// The parent component's product object is now permanently changed!
// Any other component reading product.price sees the wrong value.Because JavaScript passes objects by reference, mutating product inside DiscountBadge modifies the same object the parent component holds in memory. Every component that reads that object now sees the corrupted value. React had no chance to reconcile the change — from React's perspective, nothing changed (the reference is the same).
The Correct Approach: Derive, Don't Mutate
Instead of modifying the prop, compute a new value and use it locally:
// ✓ CORRECT — derive a new value, leave the prop untouched
function DiscountBadge({ product }) {
const salePrice = product.price * 0.9 // local derived value
return <span>Sale: ${salePrice.toFixed(2)}</span>
}
// product.price is still the original value — parent is unaffectedI Need to Change This Prop — What Should I Do?
When a child component genuinely needs to change data that came from a parent, the answer is always one of two patterns:
Lift state up — move the data into the parent as state, and pass a setter function down as a prop. The child calls the setter; the parent updates state; props flow back down.
Local copy — if the modification is purely presentational (the parent does not need to know), copy the value into local state with
useStateand modify the copy.
Pattern 1: Lift State Up
// The parent owns the data as state and passes a callback down
function ProductPage() {
const [quantity, setQuantity] = useState(1)
return (
<div>
<p>Quantity: {quantity}</p>
<QuantityPicker value={quantity} onChange={setQuantity} />
</div>
)
}
// The child reads the value from props and calls the callback to request a change
function QuantityPicker({ value, onChange }) {
return (
<div>
<button onClick={() => onChange(value - 1)} disabled={value <= 1}>-</button>
<span>{value}</span>
<button onClick={() => onChange(value + 1)}>+</button>
</div>
)
}
// QuantityPicker never touches 'value' directly —
// it only calls onChange and lets the parent decide the new statePattern 2: Local State Copy
// The parent doesn't care about internal edits until 'Save' is clicked
function ProfileEditor({ user }) {
// Copy the prop into local state — edits stay local until saved
const [localName, setLocalName] = useState(user.name)
function handleSave() {
// Parent learns about the change only via this callback
saveToServer({ ...user, name: localName })
}
return (
<form>
<input
value={localName}
onChange={(e) => setLocalName(e.target.value)}
/>
<button type="button" onClick={handleSave}>Save</button>
</form>
)
}
// user.name (the prop) is never touched — localName is a separate piece of stateWhy This Rule Makes Apps More Predictable
The immutability of props is what makes React's one-way data flow so powerful. When you see a component rendering something unexpected, you know exactly where to look: follow the props up the tree to find the parent that owns the data. If props were mutable, any component anywhere could silently corrupt the data, and debugging would become a nightmare.
Predictability — the same props always produce the same render output
Traceability — data changes can only come from state updates, which have a clear owner
Testability — pure components are trivially unit-tested by calling them with different props
Concurrency safety — React 18+ may render the same component multiple times; mutation would cause race conditions