Logo
Published on

Nested Objects

How Nested Objects Break Shallow Copying

The spread operator's shallow nature becomes a critical limitation when dealing with nested objects. While it copies the first level perfectly, nested properties remain shared references, leading to unexpected mutations deep within your data structures. This behavior catches many developers off guard in complex state management scenarios.

TL;DR

  • Spread only copies the first level: ...obj
  • Nested objects remain shared references between copies
  • Requires recursive spreading or deep clone utilities
  • Critical issue for Redux state and React component props
const shallow = { ...obj }

The Nested Reference Trap

You're managing a shopping cart with nested product details and user preferences. After using spread to "copy" the cart, updates to product quantities in one cart mysteriously affect another user's cart. The spread operator only copied the top level - all nested objects still share references.

// The problematic nested sharing
const cart1 = {
  userId: 'user-123',
  items: {
    product1: { name: 'Laptop', qty: 1, price: 999 },
    product2: { name: 'Mouse', qty: 2, price: 25 },
  },
  totals: { subtotal: 1049, tax: 104.9 },
}
const cart2 = { ...cart1 } // Shallow copy
cart2.userId = 'user-456' // First level: OK
cart2.items.product1.qty = 3 // Nested: SHARED!
const qty = cart1.items.product1.qty
console.log('Cart1 laptop qty:', qty) // 3 (bad!)

Proper nested copying requires spreading at each level or using deep clone techniques:

// The proper nested copy solution
const cart1 = {
  userId: 'user-123',
  items: {
    product1: { name: 'Laptop', qty: 1, price: 999 },
  },
  totals: { subtotal: 999, tax: 99.9 },
}
const cart2 = {
  ...cart1,
  items: { ...cart1.items, product1: { ...cart1.items.product1 } },
  totals: { ...cart1.totals },
}
cart2.items.product1.qty = 3
console.log('Cart1 qty:', cart1.items.product1.qty) // 1

Best Practises

Use nested spreading when:

  • ✅ Updating specific nested properties in React/Redux state
  • ✅ Creating variants of configuration objects
  • ✅ Working with predictable, known object structures
  • ✅ Performance is important and full deep cloning is overkill

Avoid when:

  • 🚩 Object depth is unknown or highly variable
  • 🚩 Dealing with circular references or complex data graphs
  • 🚩 Objects contain non-plain data (Dates, RegExp, functions)
  • 🚩 The nesting level exceeds 2-3 levels deep

System Design Trade-offs

AspectShallow SpreadNested SpreadDeep Clone
ComplexitySimple - one operatorManual for each levelLibrary or recursive
PerformanceFast O(n) first levelSlower O(n*m) levelsSlowest O(n*depth)
ReliabilityFails silently on nestedExplicit but verboseHandles all cases
MaintenanceEasy but dangerousError-proneDependency required
Memory UsageMinimalModerateMaximum duplication
Type SafetyGood with TypeScriptMaintainedMay lose types

More Code Examples

❌ Nested mutation nightmare
// Traditional approach with nested reference problems
function updateUserPreferences() {
  const baseConfig = {
    user: {
      id: 1,
      profile: {
        name: 'Alice',
        settings: {
          theme: {
            mode: 'light',
            colors: {
              primary: '#007bff',
              secondary: '#6c757d',
            },
          },
          notifications: {
            email: true,
            push: false,
            frequency: 'daily',
          },
        },
      },
    },
    metadata: {
      lastUpdated: new Date(),
      version: '1.0.0',
    },
  }
  // Dangerous shallow copy
  const userCopy = { ...baseConfig }
  const backup = { ...baseConfig }
  // These modifications affect all "copies"
  userCopy.user.profile.settings.theme.mode = 'dark'
  userCopy.user.profile.settings.theme.colors.primary = '#28a745'
  userCopy.user.profile.settings.notifications.email = false
  const baseMode = baseConfig.user.profile.settings.theme.mode
  const backupMode = backup.user.profile.settings.theme.mode
  console.log('Base theme:', baseMode)
  console.log('Backup theme:', backupMode)
  // Arrays inside objects also problematic
  const dashboard = {
    widgets: [
      { id: 1, type: 'chart', data: [10, 20, 30] },
      { id: 2, type: 'table', data: [40, 50, 60] },
    ],
  }
  const dashCopy = { ...dashboard }
  dashCopy.widgets[0].data.push(40)
  console.log('Original widget data:', dashboard.widgets[0].data)
  console.log('Modified length:', dashboard.widgets[0].data.length) // 4!
}
updateUserPreferences()
✅ Proper nested copying
// Modern approach with complete nested copying
function updateUserPreferences() {
  const baseConfig = {
    user: {
      id: 1,
      profile: {
        name: 'Alice',
        settings: {
          theme: { mode: 'light', colors: { primary: '#007bff' } },
          notifications: { email: true, push: false },
        },
      },
    },
  }
  // Proper deep copy using nested spread
  const userCopy = {
    ...baseConfig,
    user: {
      ...baseConfig.user,
      profile: {
        ...baseConfig.user.profile,
        settings: {
          ...baseConfig.user.profile.settings,
          theme: {
            ...baseConfig.user.profile.settings.theme,
            colors: { ...baseConfig.user.profile.settings.theme.colors },
          },
        },
      },
    },
  }
  userCopy.user.profile.settings.theme.mode = 'dark'
  console.log('Base:', baseConfig.user.profile.settings.theme.mode)
  console.log('Copy:', userCopy.user.profile.settings.theme.mode)
  // Helper function for deep copying
  function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') return obj
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof Array) return obj.map((item) => deepCopy(item))
    const cloned = {}
    for (const key in obj) {
      cloned[key] = deepCopy(obj[key])
    }
    return cloned
  }
  const deepClone = deepCopy(baseConfig)
  deepClone.user.profile.settings.notifications.push = true
  const origPush = baseConfig.user.profile.settings.notifications.push
  const clonePush = deepClone.user.profile.settings.notifications.push
  console.log('Original push:', origPush)
  console.log('Clone push:', clonePush)
}
updateUserPreferences()

Technical Trivia

The Redux time-travel debugger crisis: In 2017, a popular Redux DevTools extension was causing applications to freeze. The issue traced back to shallow copying in reducers - nested state mutations were corrupting the time-travel history, creating circular references that crashed the debugger when attempting to serialize state.

Why shallow copy exists: The spread operator was designed for performance, copying only what's necessary. Deep copying every object by default would be prohibitively expensive for large applications. JavaScript's committee chose developer control over automatic safety, leading to today's careful balance of shallow and deep operations.

The structuredClone solution: In 2022, browsers finally implemented native structuredClone() for deep copying, handling circular references, Dates, RegExp, and other complex types. Before this, developers relied on JSON.parse(JSON.stringify()) hacks or libraries like Lodash, both with significant limitations.


Master nested object copying by understanding your data structure's depth and choosing the appropriate strategy. Use nested spread for predictable 2-3 level structures in React/Redux. For complex or unknown depths, adopt structuredClone() or battle-tested libraries. Always test mutations at multiple levels to catch reference sharing bugs before they reach production.