Skip to the content.

Deep clone a value

A reducer needs to copy incoming API data before storing it, so later mutations by other code cannot reach into the store.

Implement deepClone(value):

Workspace

Edit the module below and run the question's tests. Press Ctrl+Enter (or Cmd+Enter on a Mac) to run from the editor. Your draft is kept in this browser.

Solution

Approach 1: recursion with a seen-map

export default function deepClone(value, seen = new Map()) {
  if (value === null || typeof value !== "object") return value;
  if (seen.has(value)) return seen.get(value);

  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);

  if (Array.isArray(value)) {
    const out = [];
    seen.set(value, out);                      // BEFORE recursing
    for (const item of value) out.push(deepClone(item, seen));
    return out;
  }
  if (value instanceof Map) {
    const out = new Map();
    seen.set(value, out);
    for (const [k, v] of value) out.set(deepClone(k, seen), deepClone(v, seen));
    return out;
  }
  if (value instanceof Set) {
    const out = new Set();
    seen.set(value, out);
    for (const v of value) out.add(deepClone(v, seen));
    return out;
  }

  const out = Object.create(Object.getPrototypeOf(value));
  seen.set(value, out);
  for (const key of Reflect.ownKeys(value)) out[key] = deepClone(value[key], seen);
  return out;
}

The whole exercise is the one line order matters on: seen.set(value, out) happens before recursing into children. Register after, and a cycle recurses forever. Register before, and a child that points back at its parent finds the half-built clone and links to it — which is exactly the structure you want.

That same map is what preserves shared references. Clone {a: x, b: x} without it and you get two distinct copies of x; with it, both properties point at one clone, like the original.

Approach 2: structuredClone

export default function deepClone(value) {
  return structuredClone(value);
}

Built in, handles cycles, Dates, Maps, Sets, TypedArrays and Blobs, and is faster than anything you will write. Use it when you can.

It is not always a drop-in. It throws on functions, DOM nodes, WeakMap and symbols, it drops the prototype (a class instance comes back as a plain object), and it loses non-enumerable and accessor properties. For API payloads — plain JSON-shaped data — none of that matters and it is the right answer. For arbitrary application state it can throw on something you did not know was in there.

JSON.parse(JSON.stringify(x)) is the third option and the one to avoid: it silently drops undefined, functions and symbols, turns Date into a string and NaN/Infinity into null, and throws on a cycle.

Trade-offs

Do you need a deep clone at all? Usually not. Immutable updates with spread copy only the path you changed and share everything else, which is cheaper and is what makes reference equality a useful signal for selectors and memoised components. Deep-cloning an API response on every action defeats that: every object is new, so every downstream memo misses.

The legitimate use is a trust boundary — data from outside your control that something else may still mutate — and there, cloning once at the edge is right.

Prototypes. Preserving the prototype with Object.create keeps class instances working and is usually what you want for domain objects; it also means you can clone something with a getter that throws. Plain-object output is safer and lossier.

Cost. Deep cloning is O(nodes) in time and memory. On a 5,000-item response in a reducer that runs on every keystroke, that is the performance bug — and it will not show up on your fixture of three.