Handling Immutable Data Structures with Lodash
Managing immutable state is essential for predictable application
behavior, particularly in modern front-end frameworks like React. While
standard Lodash includes several mutating methods and heavy utilities
like deep cloning, Lodash provides a specialized functional programming
variant (lodash/fp) and non-destructive patterns that make
immutable data management both clean and performant. This guide covers
how to efficiently update, add, and remove data without mutating
original structures.
The Problem with Standard Lodash and Deep Cloning
A common antipattern in JavaScript is using
_.cloneDeep() before making updates to avoid mutation:
// Inefficient pattern
const nextState = _.cloneDeep(state);
nextState.user.profile.name = "Alice";While this guarantees immutability, deep cloning duplicates the entire object tree in memory. For large structures or frequent updates (such as in an application state store), this causes severe performance bottlenecks and garbage collection churn.
True structural immutability requires modifying only the targeted node and its direct ancestors, reusing untouched references (structural sharing).
Using
lodash/fp for Automatic Immutability
The most efficient way to achieve immutability with Lodash is to use
its functional programming module, lodash/fp. Unlike
standard Lodash, lodash/fp:
- Automatically returns new shallow copies of objects and arrays rather than mutating them in place.
- Employs an iteratee-first, data-last signature.
- Curries functions by default.
To use it, import directly from the FP path:
import set from 'lodash/fp/set';
import update from 'lodash/fp/update';
const state = {
user: {
id: 1,
profile: { name: "Bob", role: "Viewer" }
},
settings: { theme: "dark" }
};
// Creates a new state with structural sharing
const nextState = set('user.profile.name', 'Alice', state);
console.log(state.user.profile.name); // "Bob"
console.log(nextState.user.profile.name); // "Alice"
console.log(state.settings === nextState.settings); // true (reference preserved)By preserving identical references for untouched branches (such as
settings), lodash/fp avoids unnecessary
re-renders in UI components.
Common Immutable Operations
1. Modifying Nested Properties
Use set to assign values or update to
compute a new value based on the existing one:
import update from 'lodash/fp/update';
const state = { counters: { score: 10 } };
const nextState = update('counters.score', n => n + 1, state);2. Removing Properties
Use unset or omit to produce new objects
without the specified keys:
import omit from 'lodash/fp/omit';
import unset from 'lodash/fp/unset';
const user = { id: 1, token: 'secret', name: 'Alice' };
// Removes top-level properties
const publicUser = omit(['token'], user);
// Removes nested properties immutably
const stateWithoutToken = unset('user.token', { user });3. Working with Arrays
Avoid mutating array operations such as push,
splice, or sort. Instead, use Lodash’s FP
array utilities:
import concat from 'lodash/fp/concat';
import filter from 'lodash/fp/filter';
import map from 'lodash/fp/map';
const list = [1, 2, 3];
// Add an item
const added = concat(list, 4); // [1, 2, 3, 4]
// Remove an item
const removed = filter(x => x !== 2, list); // [1, 3]
// Update an item immutably
const modified = map(x => (x === 2 ? 20 : x), list); // [1, 20, 3]Performance Best Practices
- Avoid
cloneDeep: Rely on path-based FP functions (set,update,unset) which perform shallow copies only along the updated path. - Import Individual Functions: Avoid importing the
entire library (
import _ from 'lodash/fp'). Import only the required functions (e.g.,import set from 'lodash/fp/set') to enable effective tree-shaking in production bundles. - Combine with Native Syntax: For single-level
updates, native JavaScript spread syntax
(
{ ...obj, key: value }) is faster than Lodash functions. Reserve Lodash FP tools for deeply nested paths or composition pipelines.