Lodash _.merge() method
What you’ll learn
- How
_.merge(object, ...sources)recursively merges plain objects into the destination. - Why it mutates the first argument—and the
_.merge({}, a, b)pattern that sidesteps it. - How
undefined,null, and array values behave when merging. - When to switch to _.mergeWith() for array concatenation or custom rules.
Prerequisites
You should know the difference between a shallow copy (_.assign, spread) and a deep recursive merge, and understand object identity vs. value.
- Mutates the destination: the first argument is written into and also returned.
- Recursive for plain objects: nested objects are merged property by property, not replaced wholesale.
- Arrays merge by index: rarely what you want.
_.mergeWithexists for a reason.
Overview
_.merge walks each source object’s own enumerable string-keyed properties (including inherited ones) and writes them into the destination. When both sides hold plain objects at the same key, it recurses. The most important fact to internalize: the destination is mutated, so the _.merge({}, defaults, overrides) idiom is the standard way to get a fresh result.
Deep, recursive
Nested plain objects are merged property by property instead of replaced.
Mutates & returns
The first argument absorbs the merge result and is the function’s return value.
undefined is skipped
Source undefined never overwrites; null, 0, "" all do.
Syntax
_.merge(object, [...sources]) - object: the destination, which is mutated and returned.
- sources: one or more source objects merged in left-to-right order. Later sources override earlier ones at the same key.
- Returns: the same destination object reference, now containing the merged result.
Deep merge with default config
The defaults-and-overrides pattern—use _.merge({}, defaults, overrides) so the defaults object survives intact.
import merge from "lodash/merge";
const defaults = {
theme: { mode: "light", fontSize: 16 },
features: { analytics: true, beta: false }
};
const overrides = {
theme: { mode: "dark" },
features: { beta: true },
user: "ada"
};
const config = merge({}, defaults, overrides);
// -> {
// theme: { mode: "dark", fontSize: 16 }, // deep-merged
// features: { analytics: true, beta: true }, // deep-merged
// user: "ada" // added
// }
defaults.theme.mode;
// -> "light" (defaults object is untouched thanks to the {}) Mutation: the destination is the result
If you forget the {} first argument, your "destination" is the thing being modified. This is a frequent source of unexpected state changes in reducers and selectors.
import merge from "lodash/merge";
const user = {
name: "John",
address: { city: "New York", country: "USA" }
};
const updates = {
age: 35,
address: { city: "San Francisco" }
};
const result = merge(user, updates);
result === user;
// -> true (same reference)
user.address.city;
// -> "San Francisco" (user was mutated)
// Immutable-style pattern:
const fresh = merge({}, user, updates);
fresh === user;
// -> false (new object) Arrays merge by index; undefined is skipped
Two non-obvious rules that bite developers when merging real data. Arrays are merged positionally (not concatenated), and an undefined source value won’t overwrite the destination.
import merge from "lodash/merge";
import mergeWith from "lodash/mergeWith";
// Arrays merge positionally (NOT concatenated):
merge({}, { items: [1, 2, 3] }, { items: [10] });
// -> { items: [10, 2, 3] }
// To concatenate, use _.mergeWith:
mergeWith({}, { items: [1, 2, 3] }, { items: [10] }, (objValue, srcValue) =>
Array.isArray(objValue) ? objValue.concat(srcValue) : undefined
);
// -> { items: [1, 2, 3, 10] }
// undefined is skipped, null overwrites:
merge({}, { name: "John", role: "admin" }, { name: undefined, role: null });
// -> { name: "John", role: null }
// name kept because undefined was skipped;
// role overwritten because null is an explicit value. 📋 _.merge vs related operations
| Topic | _.merge | _.mergeWith | _.assign | Spread { ...a, ...b } |
|---|---|---|---|---|
| Depth | Recursive | Recursive | Shallow | Shallow |
| Mutates first arg | Yes | Yes | Yes | No |
| Arrays | Merged by index | Customizer decides | Replaced | Replaced |
undefined sources | Skipped | Skipped (unless customizer returns) | Overwrites | Overwrites |
| Walks prototype | Yes (enumerable) | Yes (enumerable) | No (own only) | No (own only) |
Use _.merge for deep configuration objects with primitives and plain objects. Use _.mergeWith when arrays need to concatenate or any field needs custom semantics. Use _.assign or spread when you want a shallow, last-write-wins overlay.
Pitfalls to avoid
Don’t merge into shared state
If defaults is a module-level constant, _.merge(defaults, overrides) permanently rewrites it. Always use _.merge({}, defaults, overrides) in reducers and selectors.
Positional array merge is rarely what you want
The old reference incorrectly claimed _.merge doesn’t handle arrays. It does—by index. For concat or replace semantics, switch to _.mergeWith or pre-process arrays.
Prototype pollution surface
Never feed untrusted JSON straight into _.merge targeting a shared object. Keys like __proto__ or constructor.prototype in attacker-controlled sources can poison the runtime.
Circular references explode
Cyclic graphs lead to a stack overflow. Detect cycles up front or flatten the structure before merging.
❓ FAQ
Summary
- Purpose: recursively merge sources into the destination object, mutating it in place.
- Remember: arrays merge by index,
undefinedis skipped, and_.merge({}, ...)is the immutable-style pattern. - Next: Lodash _.mergeWith() for customizers, _.assign() for shallow merge, or the official Lodash docs for _.merge.
_.merge mutates the first argument and returns it. Pass _.merge({}, defaults, overrides) when you need an immutable-style result—the empty {} absorbs the writes.
6 people found this page helpful
