← Back to the library
FoundationsCore · 5 min

An object changes somewhere you never touched

“Defaults, cached responses, or React state change between requests or renders. One user's header shows up on another user's call, or a list is re-sorted in a component that never sorted it.”

THE PRINCIPLE

Data you did not create is read-only to you. Copy exactly what you change, and make shared values (defaults, caches, state) immutable by construction.

FIRST MOVES

  1. Find the writer: Object.freeze the suspect object in development (freeze is shallow, so freeze nested parts too) and the TypeError in strict code points at the mutation.
  2. Audit copies: { ...obj } shares nested objects, Array(n).fill({}) shares one object n times, and module-level defaults are shared by every request.
  3. Replace mutating array methods on shared data (sort, reverse, splice) with toSorted, toReversed, toSpliced and with.
  4. Use structuredClone for a deep copy of plain data; it keeps Dates, Maps, Sets and cycles, but drops prototypes and private fields and throws on functions.
  5. Type shared values as readonly / ReadonlyArray so new mutations fail at compile time.

TOOLS: THEN → NOW

JSON.parse(JSON.stringify(x)) for deep copiesstructuredClone, which also preserves Date, Map, Set and circular references
In-place sort/splice/reversetoSorted/toSpliced/toReversed/with (ES2023)
Immutability by conventionreadonly types, Object.freeze in development, or Immer for nested updates

PATTERN SNAPSHOT

const DEFAULTS: { retries: number; headers: Record<string, string> } = {
  retries: 3,
  headers: { "x-app": "web" },
};

function buildRequest(requestId: string) {
  const cfg = { ...DEFAULTS };              // shallow: cfg.headers === DEFAULTS.headers
  cfg.headers["x-request-id"] = requestId;  // every later request now carries this id
  return cfg;
}

// Fix: copy the level you change, or deep-clone plain data.
const cfg = { ...DEFAULTS, headers: { ...DEFAULTS.headers, "x-request-id": requestId } };
const copy = structuredClone(DEFAULTS);
const newest = orders.toSorted((a, b) => b.createdAt - a.createdAt); // orders untouched

CLOSE THE AI. EXPLAIN THIS.

Which built-in copy operations are shallow, and what does structuredClone deliberately not copy?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed