How do you update one field deep inside nested state without mutating it, and why is that cheap?
answer
- rebuild the path, share the rest
- copies follow depth, not size
- untouched branches keep identity
- structural sharing, not deep clone
- old root is still a valid snapshot
basics
~20 sReplace only the objects on the path from the root to the changed field, and reuse untouched branches by reference. That structural sharing copies a few objects, not the tree, and leaves unchanged branches identity-equal so dependents still skip work.
solid answer
~50 sYou rebuild the **path, not the tree**. Copy the root, overriding the one child that leads towards the change; copy that child the same way; continue down until you reach the object that owns the field, and set the new value on its copy. Every branch that is not on that path is carried over by reference, so it keeps its identity — that reuse is **structural sharing**, and it is why the update costs copies proportional to the path's depth rather than to the size of the state. The reuse is not only a saving: because untouched branches are identity-equal to before, seams comparing them still answer "unchanged" and their dependents skip work, while the branches on the path correctly report a change. Writing this by hand is noisy at depth, which is why draft-style helpers exist to write it for you.
code
pseudocode · 10 linesstate = { user: { id: 7, profile: { city: "Porto" } }, items: [ itemA, itemB ] }
# replace only the objects on the path root -> user -> profile
next = copy(state,
user: copy(state.user,
profile: copy(state.user.profile, city: "Lisbon")))
same_object(next.items, state.items) # true - untouched branch, shared
same_object(next.user, state.user) # false - on the changed path
same_object(next, state) # false - the root must be newgo deeper
Learn the move itself: copy each object from the root down to the field you are changing, set the value on the last copy, and leave everything else pointing where it already pointed.
Be able to explain the cost — copies proportional to path depth, not data size — and why the shared branches keeping their identity is what lets dependents legitimately skip work.
Recognise the half-updated screen as a path copy with one mutated level, and treat deep nesting as a data-shape problem: flatten it rather than writing longer copy chains.
Decide where the discipline is enforced — a state layer that owns updates, a draft-style helper, or normalised shapes — so correctness does not depend on every author remembering to copy every level.
## The shape of the problem State is rarely flat. A root object holds a list of items, a user object holds a profile, the profile holds a set of preferences. Now one deep field changes — a city, a flag on the third item. Under reference comparison, the seam that matters compares the value it was given, and if that value is the root, the root must be a **different object** or nothing happens at all. So a deep change has to be visible at the top. The wrong answers at each extreme are easy to name: write into the deep object (nothing sees the change) or clone the whole tree (everything sees a change, including the parts that did not change). ## Replace the path, share the rest The technique in the middle: 1. start at the root and copy it, overriding the single child that leads towards the change; 2. copy that child, overriding *its* child on the path, and repeat down each level; 3. at the bottom, copy the object that owns the field and set the new value on the copy; 4. carry every branch that is not on the path over by reference, untouched. The number of new objects equals the depth of the path — typically two to four — regardless of how many items, keys or branches the state holds. Sharing the rest by reference is what makes this cheap, and the technique has a name: **structural sharing**. The previous root is still a complete, valid snapshot of the state before the change, because nothing reachable from it was written to. ## Sharing is the point, not just the saving The allocation saving is the smaller half of the benefit. The larger half is that identity now carries information: - a branch that did not change is the **same object** as before, so any seam comparing it answers "unchanged" and its dependents legitimately skip work; - a branch on the path is a **new object**, so the chain of seams from the root down to the change all report a change — exactly the ones that should; - a deep clone would have made every branch new, producing correct output through entirely needless work, which is the second bug this leaf is about; - the old root stays intact, so comparing the previous and the current state — or keeping a short history — costs nothing beyond holding the reference. The same rule governs lists. To change one item, build a new array with that item's replacement in place and the other item objects shared; to add or remove, build a new array. An in-place add, remove, sort or reverse keeps the array's identity and so stays invisible at a reference seam. A new array holding mostly the same item objects is the best of both: the list reports a change, while rows whose item object is unchanged can skip their own work. ## Writing it by hand versus with a helper | approach | what you write | what to watch for | |---|---|---| | hand-written path copy | one copy per level, each overriding one child | noisy at depth, and skipping a level silently mutates that level | | a helper that records writes on a draft | mutating-looking code against a temporary draft, which yields a new value | the draft is a stand-in, not the state; holding it after the update, or mutating the real value alongside it, defeats the mechanism | | deep clone of the whole tree | one call, no thought | cost proportional to the data on every update, sharing destroyed, and values a generic walk cannot reproduce come back wrong | A draft-style helper is worth understanding rather than just using: it hands your code a stand-in that records every write, then produces a value with the same path-copying and sharing you would have written by hand. It changes the ergonomics, not the semantics. ## Where the discipline bites in practice - **A forgotten level.** If one level on the path is mutated instead of copied, that subtree's own dependents keep seeing the old identity while the root reports a change; the symptom is a partially updated screen. - **A stale reference held elsewhere.** Code that captured a branch from an earlier snapshot keeps reading the old data, quite correctly — it is holding a different object now. Read from the current root instead of caching branches. - **Depth as a smell.** When path copying becomes painful, the data shape is usually the problem: deeply nested state flattened into keyed lookups turns a four-level path into a one-level one, and the copying disappears with it. - **Mixed disciplines.** In a runtime that intercepts writes on a tracked handle, assigning to the deep field *is* the supported update. Applying snapshot discipline there is harmless; applying mutation discipline under snapshot comparison is the bug in this leaf's first question.
- Why not simply deep-clone the state on every update to be safe?Because it costs time proportional to the whole tree on every update and destroys sharing: every branch becomes a new object, so every seam reports a change and dependents that should have skipped work all re-run. A generic clone also mangles values it does not understand and can fail on cycles.
- What breaks if one level of the path is mutated instead of copied?That level keeps its old identity while its parent chain is new. Seams above it see a change, seams comparing that branch see none, so part of the screen updates and part does not. The previous snapshot is also corrupted, since it shares the mutated object.
- Does replacing an item in a list force every row's work to re-run?Not if the other item objects are shared. The array is new, so the list's own seam reports a change, but each unchanged row still receives the identical item object and its comparison answers "unchanged". Rebuilding every item object is what would force all rows to re-run.
Think of a filing cabinet where drawers can be shared between two cabinets. To change one folder you build a new cabinet and a new drawer for that folder's path, and point at every other drawer exactly where it already stands.
saying these in an interview costs you the question
- Believes an immutable update requires deep-cloning the whole state.
- Copies the deep object but hands the old root back to the state holder.
- Sorts or splices the array in place and expects the list to update.
- Caches a branch from an old snapshot and calls the state stale.
- Thinks a draft-style helper mutates the real state under the hood.