How to Build Immutable Reducers with Lodash FP
This article explores how developers achieve strictly immutable, pure
state transformations in reducers using the functional programming
variant of Lodash (lodash/fp). You will learn the specific
architectural mechanics of lodash/fp, why standard Lodash
falls short of immutability, and how functions like fp.set
and fp.update cleanly enforce functional purity when
managing complex application state.
The Mechanism: Lodash/fp
Vanilla Lodash (lodash) is inherently mutable for
path-based operations. Methods like _.set or
_.merge mutate the source object directly, making them
unsuitable for pure state management patterns such as Redux
reducers.
To strictly enforce immutability using only Lodash, you must use the
functional programming submodule:
lodash/fp.
lodash/fp alters the core behavior of the library in
three fundamental ways:
- Immutable by Default: Methods create structural shallow-to-deep clones of altered paths instead of modifying the target object.
- Data-Last Architecture: The target data object is always the final argument, enabling effortless functional composition.
- Auto-Curried: Every method is curried automatically, allowing path definitions and transformation logic to be pre-configured.
Key Functions for Immutable Reducers
The primary functions in lodash/fp that enforce pure
updates are:
fp.set(path, value, object): Returns a new object reference with the value at the specified path updated, preserving the original object's reference and unchanged branches.fp.update(path, updaterFunction, object): Accepts an updater callback to transform the existing value at a nested path into a new value without direct reassignment.fp.unset(path, object): Returns a copy of the object with the specified property removed.fp.flow(...fns): Composes multiple state modifications sequentially, passing the updated state from one pure function to the next.
Implementing an Immutable Reducer
Below is an example of a reducer utilizing lodash/fp to
guarantee that all updates remain purely functional and dynamically
mapped.
import fp from 'lodash/fp';
const initialState = {
users: {
byId: {
'user-1': { name: 'Alice', active: false }
},
allIds: ['user-1']
},
meta: {
lastUpdated: null
}
};
export function userReducer(state = initialState, action) {
switch (action.type) {
case 'ACTIVATE_USER':
// Dynamically paths into nested properties and returns a fresh object reference
return fp.flow(
fp.set(['users', 'byId', action.payload.id, 'active'], true),
fp.set(['meta', 'lastUpdated'], action.payload.timestamp)
)(state);
case 'UPDATE_USER_NAME':
// Dynamically updates an existing value cleanly via transformation logic
return fp.update(
['users', 'byId', action.payload.id, 'name'],
() => action.payload.newName,
state
);
case 'REMOVE_USER':
return fp.flow(
fp.unset(['users', 'byId', action.payload.id]),
fp.update(['users', 'allIds'], (ids) => ids.filter(id => id !== action.payload.id))
)(state);
default:
return state;
}
}Why This Enforces Strict Purity
- Structural Sharing:
lodash/fpcopies only the nodes along the path being altered while retaining unmodified object references, satisfying the performance requirements of pure components and change-detection systems. - Zero In-Place Side Effects: The original
statepassed to the reducer is never touched. CallingObject.is(state, newState)evaluates tofalsewhen an action produces a mutation, andtruewhen the default branch returns the previous state. - Pointfree Composition: By pairing
fp.flowwith curried path functions, actions apply multiple deep transformations without maintaining intermediary local variables, eliminating risks of accidental in-place reassignments.