Course outline

React / Components and state / Lesson 4

Immutable state and derived values

Update objects and arrays without mutating earlier snapshots, and avoid storing values that can be calculated from existing state.

About 4 min readContent reviewed
On this page

Lesson goals

  • Treat past state as read-only
  • Replace the changed object
  • Test transitions as domain behavior

Treat past state as read-only

State may contain an object, but changing one of its properties in place does not create a new state value. React relies on identity comparisons in important paths, and prior renders or closures can still refer to that object. Mutation therefore damages both update detection and the snapshot model. Create a replacement object for a changed record and preserve untouched values where appropriate. This is not a demand to deep-copy the entire application on every keystroke. Copy the path of changed ownership deliberately and keep the state shape simple enough that those paths remain understandable.

Choose operations with explicit ownership

Object spread creates a shallow copy, so nested objects remain shared unless you replace them too. Array map, filter, and spread can construct replacements for edits, removals, and additions. Methods such as push, sort, and reverse mutate their array; use a copied array when that mutation is a deliberate implementation choice. A new outer array is insufficient if an item object is then modified in place. Identify the changed item by a stable domain id and return a new item only for that match. Clear ownership prevents one component’s edit from changing another view’s previously captured data.

Replace the changed object

The first example increments a nested score by copying both the profile and its stats object. The updater reads only its prior input and returns a coherent new value. Unchanged fields such as the name are retained. A shallow copy of the profile followed by next.stats.score++ would still mutate the original nested stats. Test the earlier snapshot independently when writing pure transition helpers. The visible result should be the same whether React repeats the calculation in development or not, because the calculation itself does not alter any object that existed before it.

jsx Component module — requires the course React host
import { useState } from "react";
export default function App() { const [profile, setProfile] = useState({ name: "Mira", stats: { score: 2 } }); return <main><h1>{profile.name}</h1><p>{profile.stats.score}</p><button type="button" onClick={() => setProfile(p => ({ ...p, stats: { ...p.stats, score: p.stats.score + 1 } }))}>Add point</button></main>; }
Expected output
Mira. Initial score: 2. After Add point: 3.

Both levels on the changed path are replaced. One click raises the visible score from two to three without changing the name.

Derive information during render

The second example stores only the records and derives their total during rendering. Storing both the records and total would create a synchronization obligation: every edit, deletion, reset, and incoming update would need to maintain their equality. A cheap pure calculation avoids that duplicated state. The same principle applies to filtered views, selected-record details found by id, and simple display labels. Memoization can later cache an expensive pure calculation, but it does not justify storing redundant state or repair an impure formula. First choose the minimal source of truth and state the invariant the derived result should satisfy.

jsx Component module — requires the course React host
import { useState } from "react";
export default function App() { const [items, setItems] = useState([{ id: "a", amount: 2 }, { id: "b", amount: 3 }]); const total = items.reduce((sum, item) => sum + item.amount, 0); return <main><h1>Total: {total}</h1><button type="button" onClick={() => setItems(xs => xs.filter(x => x.id !== "a"))}>Remove A</button></main>; }
Expected output
Initial: Total: 5. After Remove A: Total: 3.

The total is derived from the current array. Removing A yields three without a second total setter to keep synchronized.

Test transitions as domain behavior

A good transition test checks the requested new values, unchanged unrelated values, and preservation of the input snapshot. Include an absent id, an empty collection, and repeated operations under the documented policy. Do not assert that every nested object must always be new; retaining unchanged references is legitimate and often useful. Conversely, do not equate a new reference with correct business meaning. A stock decrement can be immutable and still violate the nonnegative-stock rule. Immutability supports predictable state evolution, while validation and domain constraints remain separate requirements. Keep both visible in the component or its pure reducer.

Common errors and fixes

An object changes but the interface does not update reliably

Why it happens
The existing state object was mutated and reused.
How to fix it
Create a replacement along the changed path.
Prevent it next time
Treat earlier snapshots as immutable inputs.

An old nested value changes unexpectedly

Why it happens
Spread copied only the outer object.
How to fix it
Replace the modified nested object too.
Prevent it next time
Test that the original nested value remains unchanged.

The displayed total drifts from the list

Why it happens
Duplicated state was not updated on every path.
How to fix it
Derive the total from the source collection.
Prevent it next time
Store only independently meaningful state.

Check your understanding

1. Describe an immutable update that marks one task complete by id.

Show worked solution

Map over the previous array and return a new object with complete: true only for the matching id; return each other item unchanged. Return the new array from the updater. This preserves prior snapshots and avoids changing unrelated task identities.

2. Should a component store a selected object and its id independently?

Show worked solution

Usually store the stable selected id and derive the object from the current collection. Otherwise replacing or deleting the collection’s item can leave a stale selected object. Define the missing-id behavior explicitly, such as showing no selection after deletion.

Key takeaways

  • Copy the changed path, not blindly everything.
  • Derived data avoids synchronization bugs.
  • Immutability and domain validity are separate.

Official references