Deep Copy vs Shallow Copy: Get It Right and Never Run Into Data Bugs Again
Have you ever changed a 'new' object only to see the original object change with it? This common JavaScript bug comes from confusing shallow vs deep copy. Learn how memory works and pick the right method, especially for complex state in React or Redux.

Trung Vũ Hoàng
Author
1. Value vs Reference — The Root of the Problem
JavaScript treats two groups of data types very differently:
Primitive types:
string,number,boolean... — when you assigna = b, an independent copy of the value is created. Changingadoes not affectb.Reference types:
Object,Array— when you assignobj2 = obj1, JavaScript does not create a new object. It only creates another "path" (reference) pointing to a single memory location on the Heap. This is the root of the confusion.
2. Shallow Copy — Surface-Level Copy, Danger Beneath
A shallow copy creates a new object, but only copies the top-level properties:
Primitive-typed properties → copied by value, safe.
Nested Object/Array properties → only the reference is copied, still sharing memory with the original.
Common syntax for shallow copy: { ...obj } (Spread), Object.assign({}, obj).
const original = { name: "JS", meta: { version: 1 } };
const copy = { ...original };
copy.name = "Python"; // OK — original.name remains unchanged
copy.meta.version = 99; // DANGEROUS — original.meta.version also becomes 99!Shallow copy is very useful when the object is "flat" (no nesting). But with multi-level JSON returned from an API, it’s a ticking time bomb.
3. Deep Copy — Fully Independent, No Shared Links
A deep copy walks through every level of the object and creates a brand-new clone at every level — leaving no memory links to the original. Changing the copy will never affect the original, no matter how deep the nesting.
Deep copy is commonly used for: state snapshots, Undo/Redo features, and data processing that requires strong immutability.
4. Methods to Perform a Deep Copy
JSON.parse(JSON.stringify(obj))
const deepCopy = JSON.parse(JSON.stringify(original));Fast and simple — but loses data with special types: Date, undefined, Function, RegExp. Use only when the object is guaranteed to be plain JSON.
structuredClone() — Modern API, No Library Needed
const deepCopy = structuredClone(original);Available on modern browsers and Node.js 17+. Faster than JSON and handles more types (Date, Map, Set...). This is the best choice for most cases.
Lodash _.cloneDeep()
import cloneDeep from 'lodash/cloneDeep';
const deepCopy = cloneDeep(original);The "gold standard" in large projects — handles circular references and complex data types. Suitable when your project already includes Lodash.
5. When to Use Shallow vs Deep?
Use a shallow copy when: The object is "flat" (no nesting), or you intentionally want child objects to be shared to save memory.
Use a deep copy when: You handle multi-level data, need strict immutability, or implement Undo/Redo.
6. Performance Considerations
Deep-copying a large object with thousands of elements can make the app stutter — because the engine must traverse the entire data tree.
Optimization tip: When you only need to change a small part of a nested object, use the Immutable Update Pattern (common in Redux Toolkit) — create new objects only for the levels you change, keep the old references for the rest:
const newState = {
...state,
user: {
...state.user,
name: "Updated" // Only the user level is recreated
}
};7. Summary
Distinguishing deep copy vs shallow copy is not just about syntax — it’s a mindset about memory architecture. A good developer knows exactly which memory their code touches:
Flat object + performance first → Spread / Object.assign (shallow).
Nested object + needs independence → structuredClone (deep, no library).
Large projects, complex data → Lodash _.cloneDeep (deep, most comprehensive).
Frequently Asked Questions
Bài viết liên quan

Zustand Async: 5 Effective Ways to Handle Async in React
Every real-world React app needs to communicate with async APIs. If handled poorly, it’s easy to run into issues like frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an extremely simple syntax—define async actions directly in the store, without complex middleware like Redux Thunk or Saga.

Advanced Promises: all, allSettled, race, any — When to Use Which?
If you only use sequential async/await, your app is wasting performance potential — each request has to wait for the previous one to finish before it can start. Promise’s static methods (all, allSettled, race, any) let you orchestrate multiple async tasks in parallel using the strategy that fits each problem.

Zustand Async: 5 Effective Ways to Handle Async Operations in React
Every real-world React app has to communicate with asynchronous APIs. If managed poorly, your app can run into issues like a frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an incredibly simple syntax — define async actions directly in the store, without complex middleware like Redux Thunk or Saga.