As a full-stack JavaScript developer, you‘ll inevitably need to combine data from different objects. Whether you‘re consolidating API responses on the server or Redux state in React, understanding object merging is a fundamental skill.

In this comprehensive 2600+ word guide, we‘ll explore the ins and outs of merging, including best practices to follow and pitfalls to avoid.

Why Merge Objects in JavaScript?

First, let‘s examine a few common use cases for needing to merge:

Config Objects

Most apps require configuration to wire up frameworks, integrate with services, load environment variables, and more. A common pattern is having a defaultConfig that various modules can add to:

// defaultConfig.js
module.exports = {
  apiURL: ‘https://api.domain.com‘,
  cacheTTL: 3600 
}

// databaseConfig.js 
const defaultConfig = require(‘./defaultConfig‘);

module.exports = {
  ...defaultConfig,
  dbHost: ‘localhost‘,
  dbPort: 27017  
}

This allows separating configuration concerns while sharing common settings.

Enriching API Responses

When working with external APIs, you may want to attach additional metadata or temporary values to responses:

const apiResponse = {
  id: ‘123abc‘,
  name: ‘John Doe‘,
  email: ‘john@doe.com‘ 
}

const enrichedResponse = {
  fetchedAt: Date.now(), 
  ...apiResponse,
  expiresAt: Date.now() + 360000  
}

This pattern avoids mutating the original API response while adding the necessary context.

Consolidating State

In many Frontend frameworks like React and Vue, state is often split across multiple reducers or modules. For example, you may have separate user and settings reducers that need to be combined in a common object:

// State example in React/Redux
const commonState = {
  ...userState,
  ...settingsState 
} 

The full state object then contains data managed by different parts of the app.

Analytics Data Processing

When processing analytics data, you‘ll frequently need to join information from multiple events and entities into consolidated records:

const pageview = {
  url: ‘/‘,
  timestamp: ‘2020-01-01T00:00:00Z‘  
}

const impression = {
  adId: ‘ad12345‘,
  slot: ‘header‘,
  timestamp: ‘2020-01-01T00:05:00Z‘   
}

const trackedEvent = {
  ...pageview, 
  ...impression
}

These merged records can then be loaded into data warehouses and analytics tools.

The examples above cover just a small sample of use cases. Virtually any non-trivial JavaScript application will require combining objects.

Shallow Merging in JavaScript

The easiest way to merge objects in JavaScript is by making a shallow copy. This combines all top level keys from the source objects into a new target object.

There are two main ways to shallow merge:

1. Object.assign()

The Object.assign() method was added in ES6 to merge objects into a target object:

const target = { a: 1, b: 2 } 
const source = { b: 4, c: 5 }

const merged = Object.assign(target, source)  
// { a: 1, b: 4, c: 5 }

Anything from source overwrites matching keys in target. The original target is also mutated.

To prevent mutation, pass an empty object as the first argument:

const merged = Object.assign({}, target, source) 

You can merge an unlimited number of objects by passing them as additional arguments:

const user = { name: ‘John‘ }
const age = { age: 20 } 

const merged = Object.assign({}, user, age)  
// { name: ‘John‘, age: 20 }

Performance:
Object.assign() performs better than the spread operator in production benchmarks (source). This difference is magnified when merging larger objects.

However, Object.assign() is still typically slower than specialized utility libraries like _.merge from Lodash.

2. Spread Operator

Another popular approach is using the spread (...) operator added in ES6:

const user = { name: ‘John‘ }
const age = { age: 20 }  

const merged = { ...user, ...age }
// { name: ‘John‘, age: 20 } 

Anything from the second object overrides keys from the first. You can spread an unlimited number of objects.

When to use Object.assign vs spread:

  • Object.assign() is more performant according to benchmarks.
  • Spread requires less code when merging into a new object.
  • Use spread if you need to add keys besides objects.

For example:

const data = { 
  user: userObject,
  lastUpdated: Date.now() 
}

Deep Merging Objects in JavaScript

A limitation with Object.assign() and spread is that they only make shallow copies. If objects have nested objects, the nested ones won‘t be merged.

const person = {
  name: ‘John‘,
  details: {
    age: 20,
    job: ‘software engineer‘
  }
}

const newPerson = {
  name: ‘Jane‘,
  details: {
    age: 25
  }
}

const merged = {...person, ...newPerson } 
// nested data not merged  

{
  name: ‘Jane‘, // overwritten
  details: {
    age: 25  
  }  
}

For deep merges, we need to recursively merge objects including prototypes:

function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] }); 
      }
    }
  }
  return mergeDeep(target, ...sources);
}

const John = {
  name: ‘John‘,
  details: {
    age: 20, 
    job: ‘software engineer‘ 
  }
};

const Jane = {
  name: ‘Jane‘,
  details: {
    age: 25
  }
}

const merged = mergeDeep({}, John, Jane)

// fully merged!
// {
//   name: ‘Jane‘, 
//   details: {
//     age: 25,
//     job: ‘software engineer‘  
//   }  
// }

This handles merging to any depth by crawling the object tree recursively.

Customize the function to properly handle arrays, edge cases, and input validation – or use a utility library like lodash merge.

Key things to consider with deep merging functions:

  • Immutability: By default deep merging mutates the original target object. Create defensive copies to prevent this.

  • Arrays: Properly handle cases like concatenating arrays instead of overwriting.

  • Functions and Prototypes: Include logic to check prototypes and exclude functions.

  • Circular References: Detect circular references to avoid infinite recursion.

Getting all the edge cases right is complex! Again leveraging libraries like Lodash can save development time.

Performance Considerations

Deep merging has a higher performance cost than shallow solutions due to the nested traversal. There are a few ways to improve performance:

  • memoization – cache merge results
  • benchmarks – compare alternatives like manual vs. lodash
  • keys – only merge necessary keys
  • immutable data – reuse merged objects

Merging Arrays in JavaScript

The same principles for merging objects apply to arrays, with a couple caveats:

  • Spread operators combine arrays by concatenating rather than overwriting values.
  • Nested arrays require recursively checking each element.
  • Deleting elements or maintaining order gets complex.

Here‘s an example sum of array merging with spread:

const fruits = [‘apple‘, ‘banana‘]
const vegetables = [‘carrot‘, ‘potato‘]

const produce = [...fruits, ...vegetables]  
// [‘apple‘, ‘banana‘, ‘carrot‘, ‘potato‘]

However, the spread operator has some notable limitations when working with arrays:

  • Incorrectly handles nested arrays instead of concatenating
  • Doesn‘t maintain order if arrays have duplicate values
  • Doesn‘t remove duplicate values

Deep array merging better handles these complex cases. For example:

function mergeArrays(target, source) {

  if (!Array.isArray(target) || !Array.isArray(source)) {
    return; 
  }

  source.forEach(sourceElement => {

    if (!target.includes(sourceElement)) {  
      target.push(sourceElement);
    } 

  });

  target.forEach(targetElement => {

    if (Array.isArray(targetElement)) {
      mergeArrays(targetElement, source); 
    }

  }); 

}

// Example usage:

const fruits = [‘apple‘]
const fruitBaskets = [
  [‘banana‘, ‘orange‘],
  [‘apple‘, ‘orange‘], 
]  

mergeArrays(fruits, fruitBaskets);

fruits // [‘apple‘, [‘banana‘,‘orange‘], [‘apple‘,‘orange‘]]  

This recursively checks elements, removes duplicates, and handles nested arrays.

To ensure arrays preserve ordering correctly, the comparison logic needs improvement. Look into using Map/Set objects or custom comparison functions.

Again, for robust solutions prefer battle-tested libraries like Lodash.

Merging Best Practices

Hopefully by this point you appreciate objects merging complexity! Before wrapping up, let‘s review some best practices:

1. Favor Immutability

Avoid mutating original objects. Libraries like Immer make immutable updates easier.

Defensive copying also prevents issues:

function merge(target, ...sources) {
  return mergeDeep(structuredClone(target), ...sources)  
}

2. Validate Inputs

Ensure inputs meet expectations – are they objects, properly formatted, contain required keys etc.

Type checking with PropTypes, TypeScript, or runtime validation helps.

3. Handle Edge Cases

Have special handling for arrays, functions, circular references etc.

Or…simply exclude non-enumerable keys by default.

4. Use Recursive Helper Functions

Keep the main API simple while refactoring complexity into helpers.

5. Add Comments & Documentation

Explain intended usage, edge case behavior, runtime complexity etc.

Alternative Techniques

There are a few more advanced techniques worth mentioning:

Maps and Sets

Using JavaScript‘s Map and Set objects can simplify duplicate handling and ordering for arrays and array-like structures.

However, they only hold primitive values natively so requires custom serialization.

Lodash and Utils

JavaScript utility libraries like Lodash, Mixpanel Utils, Deepmerge etc. have battle-tested merge logic.

Often easier than rolling custom tools.

JSON Parsing

An interesting approach is converting objects into JSON strings first before merging.

Allows handling duplicates and non-enumerable properties.

However, you lose live object references – so not always practical.

There are certainly other valid ways to merge objects. But hopefully the patterns here provide a solid starting point!

Recommendations Summary

Here are my key recommendations when merging objects in JavaScript:

  • Shallow merging: Use Object.assign() and Spread Operator
  • Deep merging: Implement recursive merge helper
  • Arrays: Handle order, nesting properly
  • Immutability: Use defensive copying
  • Libraries: Leverage existing utils before rolling custom
  • Validate: Guard against edge cases
  • Document: Explain usage, limitations

And that covers everything you need to know about merging objects! Let us know if you have any other questions.

Similar Posts