AniUI Academy

Update one node in a tree without cloning it

hardData Structures+350 XPPractice

Editing one comment in a deep thread stored in React state cannot mutate the tree, but deep-cloning the whole thing throws away every memoisation you have. The answer is structural sharing: recreate only the nodes on the path from the root to the one that changed, and hand back every other subtree as the exact same object.

Implement updateNode(root, id, updater). root is a node shaped { id, ...otherProperties, children } where children is an array of nodes and may be missing. Find the node whose id equals id and replace it with the result of calling updater(node), which returns a new node object.

Return the new root. Every node on the path from the root down to the changed node must be a new object, not the original. Every subtree that does not contain the changed node must be returned as the identical object it already was, so next.children[1] === root.children[1] holds for an untouched sibling. When no node matches the id, return the original root itself, unchanged and by reference. Never modify the original tree.

Arguments

root{ id: unknown; children?: Node[] } & Record<string, unknown>
The root node of the tree.
idunknown
The id of the node to update.
updater(node: Node) => Node
Called with the matching node; must return a new node object to put in its place.

Returns

Node — A new root with the matching node replaced, or the original root by reference when no node matched.

Examples

const tree = { id: "root", children: [
  { id: "a", children: [{ id: "a1", label: "A1" }] },
  { id: "b" },
]};
const next = updateNode(tree, "a1", (n) => ({ ...n, label: "EDITED" }));
next.children[0].children[0].label; // "EDITED"
tree.children[0].children[0].label; // "A1" — untouched
const next = updateNode(tree, "a1", (n) => ({ ...n, done: true }));
next.children[1] === tree.children[1];  // true — sibling shared
next.children[0] !== tree.children[0];  // false — path recreated

Untouched subtrees are shared by reference; the path is rebuilt.

const next = updateNode(tree, "missing", (n) => n);
next === tree; // true — no match, same reference

What it has to do

  • The returned tree contains the updated node.
  • Every node on the path from the root to the changed node is a new object.
  • Untouched subtrees are returned by reference, identical to the originals.
  • When no node matches, the original root is returned by reference.
  • The original tree is never mutated.

Notes

  • O(n) worst case but only O(h) nodes get rebuilt — the path from root to the match. Every sibling subtree stays shared, which is what makes React memoisation downstream still fire correctly.
  • The recursive trick: compare each child's recursed result with the child you passed in. If every child came back identical, return the current node unchanged so the no-op bubbles all the way up.
  • This pattern is what Immer, Redux reducers and most React state libraries lean on under the hood — rebuild the spine, share the rest.

Stuck? The javascript course covers everything this challenge needs.

Try it yourself
Loading playground...
5 tests · +350 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.