Update one node in a tree without cloning it
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
idof 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" — untouchedconst 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 recreatedUntouched subtrees are shared by reference; the path is rebuilt.
const next = updateNode(tree, "missing", (n) => n);
next === tree; // true — no match, same referenceWhat 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.
- 1.updates a nested node without touching the original
- 2.recreates every node along the path
- 3.shares untouched subtrees by reference
- 4.returns the same root when nothing matches
- 5.can update the root itself
These assertions run in your browser the moment you hit Run. Pass all of them to claim the XP.
Discussion
Sign in to postNo comments yet. Be the first to say something.