Stop rewriting immutable updates: normalize lists with Redux Toolkit createEntityAdapter
Use Redux Toolkit’s createEntityAdapter to store list data in a normalized {ids, entities} shape, get memoized selectors, and avoid manual immutable update logic when working with frequently changing collections.
18 Apr 2026, 22:45 UTC

The problem with flat arrays in Redux
A flat array is easy to read but expensive to maintain. Adding, updating or removing one item requires creating a new array and often a new object for each element. React components that select the list with shallow equality re-render even when only one item changed.
Normalization means storing entities in an object keyed by id and keeping a separate ordered list of ids. This indirection makes point updates cheap and selectors stable.
What createEntityAdapter gives you
createEntityAdapter is a factory from @reduxjs/toolkit that returns reducer helpers and memoized selectors for a normalized collection with shape {ids:[], entities:{}}.
The helpers cover common CRUD without manual spread logic: addOne, addMany, upsertOne, upsertMany, updateOne, updateMany, removeOne, removeMany, setAll and removeAll. They are Immer compatible.
The generated selectors are selectAll, selectById, selectIds and selectTotal. selectAll returns an array ordered by ids, and the selectors are designed to return consistent references when the underlying data has not changed, which supports efficient React rendering with shallow equality.
Idiomatic integration with createSlice
Using the adapter as the slice reducer keeps update logic declarative.
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit';
const tasksAdapter = createEntityAdapter({
sortComparer: (a,b) => a.due - b.due
});
const tasksSlice = createSlice({
name: 'tasks',
initialState: tasksAdapter.getInitialState(),
reducers: {
addTask: tasksAdapter.addOne,
updateTask: tasksAdapter.updateOne,
removeTask: tasksAdapter.removeOne,
}
});
export const { selectAll: selectAllTasks, selectById: selectTaskById } = tasksAdapter.getSelectors(state => state.tasks);
Run this in a Node or browser environment where @reduxjs/toolkit is installed. Required permission is read access to the package. Verify the state shape by inspecting state.tasks after dispatching addTask and updateTask; you should see ids as an array and entities as a map keyed by id. Check that selectAllTasks returns an array ordered by ids.
Trade-offs and limitations
Normalization adds indirection. For very small, static lists a plain array is simpler to read and debug.
The API surface and default sort behavior are version sensitive in Redux Toolkit. sortComparer and initial state shape have changed across major releases, so confirm the installed version exports createEntityAdapter and list its returned methods.
Adapter helpers assume a stable idSelector. Changing the identifier strategy requires recreating the adapter and migrating existing state, which is a breaking change.
Practical check: create a minimal reducer with createEntityAdapter, dispatch addOne then updateOne, and assert the ids array length and entities keys match expectations. Ensure selectors return the same reference when unrelated state changes.
When to use it
Choose createEntityAdapter when you have a collection that is frequently updated individually, needs stable selectors for React, or is normalized from a server response. Avoid it for transient UI lists that never need identity tracking.
Start with the adapter, keep the sortComparer explicit, and centralize selectors from getSelectors. That gives you predictable immutable updates without hand-rolling them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.