Speeding Up Redux CRUD with createEntityAdapter: A Practical Guide
Use Redux Toolkit’s createEntityAdapter to normalize collections, reduce CRUD boilerplate, and speed up lookups. Learn how to set it up, compose selectors, and weigh the trade‑offs for your app.
14 Sept 2026, 10:33 UTC

Why Normalization Matters in Redux
When you store a list of items in Redux, the simplest way is a plain array. That works fine for tiny, single‑page lists, but as soon as you need to look up an item by id or share the same collection across multiple parts of the UI, you run into two problems:
- Every lookup becomes an
O(n)scan, which hurts performance as the list grows. - Reducers that add, update, or remove items must copy the entire array and then splice the change in, creating repetitive boilerplate.
Redux Toolkit’s createEntityAdapter solves both issues by normalizing the state into a { ids, entities } shape and providing ready‑made CRUD reducers.
Getting Started: The Adapter Boilerplate
Below is a minimal slice that uses an adapter for a collection of blog posts. The slice contains an authorId field so that we can demonstrate a cross‑slice lookup.
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit';
// 1️⃣ Define the adapter – it creates the normalized shape.
const postsAdapter = createEntityAdapter();
// 2️⃣ Create the slice, wiring the adapter’s reducers.
const postsSlice = createSlice({
name: 'posts',
initialState: postsAdapter.getInitialState(),
reducers: {
addPost: postsAdapter.addOne,
updatePost: postsAdapter.updateOne,
removePost: postsAdapter.removeOne,
},
});
export const { addPost, updatePost, removePost } = postsSlice.actions;
export default postsSlice.reducer;
Notice how the slice’s reducers object is a single line per action. The adapter’s addOne, updateOne, and removeOne functions already implement the immutable update logic for you.
Selectors: From Flat to Denormalized Views
Adapters also provide a selector set. By default, the selectors look like this:
const postsSelectors = postsAdapter.getSelectors(state => state.posts);
// postsSelectors.selectById(state, id) → post object
// postsSelectors.selectAll(state) → array of posts
Because the adapter stores items in an entities map keyed by id, looking up a post is a constant‑time operation. For example, in a component you can write:
const post = useSelector(state => postsSelectors.selectById(state, postId));
Now, if each post contains an authorId that references a user in a separate slice, you can also look up the author in O(1):
const selectUserById = (state, userId) => state.users.entities[userId];
const author = useSelector(state => selectUserById(state, post.authorId));
Without adapters you would have to scan the entire users array every time you need an author.
Composing a Denormalized View
Often you want a single object that contains the post and its author. Reselect’s createSelector lets you memoize that composition so it only recomputes when either the post or the author changes.
import { createSelector } from '@reduxjs/toolkit';
const selectPostWithAuthor = createSelector(
[postsSelectors.selectById, (state, id) => state.users.entities[id]],
(post, author) => post ? { ...post, author } : undefined
);
When you use selectPostWithAuthor in a component, React will only re‑render if the post or its author object reference changes.
Trade‑Offs: Indirection vs. Simplicity
- Indirection: Consumers must compose selectors to reconstruct nested shapes. This adds a layer of complexity that can be confusing for new developers.
- Potential Re‑renders: If you forget to memoize the denormalized selector, any change in the users slice will cause the component to re‑render, even if the author hasn't changed.
- Overhead for Tiny Lists: Normalizing a list of 5–10 items adds mental and code overhead that rarely pays off in terms of performance.
Because of these points, the adapter shines when:
- The collection is large (hundreds or thousands of items).
- Multiple components or slices need to reference the same items.
- You need fast
idlookups or efficient updates.
Practical Checklist Before Adoption
- Pin your Redux Toolkit version (e.g.,
@reduxjs/toolkit@1.9.5) and read the changelog for adapter API changes. - Verify that
selectByIdreturns the same reference after unrelated upserts to confirm memoization works. - Use Redux DevTools to inspect the state shape; you should see
ids: []andentities: {}for each adapter. - Benchmark a 10k‑item array
.find()versusentities[id]if you are concerned about raw performance.
Conclusion: When to Normalize
Adopting createEntityAdapter is a pragmatic step when you:
- Need to store collections that grow beyond a few dozen items.
- Share data across multiple UI parts or features.
- Want to reduce boilerplate in reducers.
For simple, one‑off lists that appear in a single component, a plain array remains the cleanest choice. If you start with an array and later discover you need cross‑slice lookups, you can migrate to an adapter without a major refactor.
Start small: create an adapter for the first collection you notice is accessed by many components. Measure, observe the normalized state in DevTools, and iterate. Over time you’ll build a lean, fast Redux store that scales with your application.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.