How to Normalize Nested Redux State with Lodash
Managing deeply nested state in Redux often leads to convoluted reducer logic, accidental state mutations, and unnecessary component re-renders. Structural normalization resolves these issues by flattening nested hierarchies into relational, database-like structures organized by IDs. The Lodash utility library provides a robust, lightweight set of functional tools that streamline this transformation process, enabling developers to decompose nested API responses into normalized state slices without relying on heavier specialized schema-definition libraries.
The Challenge of Nested Redux State
API payloads frequently nest related resources—such as a blog post containing an author object and an array of comment objects, each with their own author. Storing this data hierarchically causes several architectural problems:
- Duplication: The same user entity appearing in multiple comments is duplicated across the store.
- Complex Reducer Logic: Updating deeply nested properties requires multiple levels of object spreading to maintain immutability.
- Performance Degradation: Immutability updates to deeply nested objects trigger selector recomputations and component re-renders across the entire parent tree.
Normalization flattens this structure into distinct entity tables indexed by ID, replacing nested objects with relational references (arrays of IDs).
Key Lodash Functions for State Normalization
Lodash provides functional data-manipulation methods that map directly to the requirements of data normalization:
1. _.keyBy
The foundation of a normalized store is the lookup table (a
dictionary indexed by entity ID). _.keyBy transforms a flat
array of entities into an object keyed by a designated property:
import keyBy from 'lodash/keyBy';
const postsArray = [
{ id: 'p1', title: 'First Post' },
{ id: 'p2', title: 'Second Post' }
];
const postsById = keyBy(postsArray, 'id');
// Result:
// {
// p1: { id: 'p1', title: 'First Post' },
// p2: { id: 'p2', title: 'Second Post' }
// }2. _.mapValues
When normalizing, parent entities must replace nested child objects
with ID references. _.mapValues iterates over an indexed
object and maps its properties to a new shape:
import mapValues from 'lodash/mapValues';
// Converting nested author objects to author IDs in a posts dictionary
const normalizedPosts = mapValues(postsById, post => ({
...post,
author: post.author.id,
comments: post.comments.map(c => c.id)
}));3. _.flatMap and
_.uniqBy
Nested entities must be extracted from their parents and collected
into their own top-level collections. _.flatMap extracts
nested collections into a single array, while _.uniqBy
removes duplicates:
import flatMap from 'lodash/flatMap';
import uniqBy from 'lodash/uniqBy';
// Extract all unique comments from an array of posts
const allComments = uniqBy(
flatMap(postsArray, post => post.comments),
'id'
);
const commentsById = keyBy(allComments, 'id');4. _.groupBy
For one-to-many relationships where an entity needs to know which
children belong to it, _.groupBy aggregates keys:
import groupBy from 'lodash/groupBy';
// Group comment IDs by their parent post ID
const commentIdsByPostId = mapValues(
groupBy(allComments, 'postId'),
comments => comments.map(c => c.id)
);Practical Implementation: Normalizing a Nested Payload
Consider an incoming API payload containing articles, embedded author profiles, and embedded comments:
import keyBy from 'lodash/keyBy';
import mapValues from 'lodash/mapValues';
import flatMap from 'lodash/flatMap';
import uniqBy from 'lodash/uniqBy';
export function normalizeArticlesPayload(articles) {
// 1. Extract and normalize authors
const authors = uniqBy(
articles.map(article => article.author),
'id'
);
// 2. Extract and normalize comments
const comments = uniqBy(
flatMap(articles, article => article.comments || []),
'id'
);
// 3. Normalize articles by replacing child objects with ID references
const processedArticles = articles.map(article => ({
id: article.id,
title: article.title,
authorId: article.author.id,
commentIds: (article.comments || []).map(comment => comment.id)
}));
// 4. Return the fully flattened slices
return {
entities: {
articles: keyBy(processedArticles, 'id'),
authors: keyBy(authors, 'id'),
comments: keyBy(comments, 'id')
},
result: articles.map(article => article.id)
};
}Safe Updates in
Reducers with _.merge and _.omit
Once state is normalized, updating it requires shallow operations. Lodash helps maintain immutability and handle dynamic updates cleanly:
_.merge: Safely combines newly received entity maps with existing state slices without overwriting unmodified fields._.omit: Removes deleted entities from lookup tables immutably:
import omit from 'lodash/omit';
function commentsReducer(state = { byId: {}, allIds: [] }, action) {
switch (action.type) {
case 'COMMENT_DELETED':
return {
byId: omit(state.byId, action.payload.id),
allIds: state.allIds.filter(id => id !== action.payload.id)
};
default:
return state;
}
}Using Lodash for state normalization gives developers granular control over data shaping without the overhead of schema-driven frameworks. Its functional utilities ensure deterministic transformations, reduce boilerplate in reducers, and optimize Redux store performance.