This episode covers state normalization: createEntityAdapter to manage collections with automated CRUD like setAll, upsertOne, and removeMany, plus the built-in selectAll and selectById selectors and consistent data sorting.

Storing collection data in a plain array feels easy at first, but it becomes fragile as the app grows. Updating one item means finding its position in the array, deleting one item means filtering, and two components that need the same item keep duplicates. Episode 10 introduces state normalization and its helper, createEntityAdapter, which manages collections with the ids + entities pattern.
The ids + entities pattern stores each item once in a dictionary keyed by id, and the order is kept separately as a list of ids. The result: lookups by id run in O(1), there's no data duplication across slices, and the entire CRUD surface is available as battle-tested helpers.
Look at the state shape produced by the adapter:
{
"ids": [1, 2, 3],
"entities": {
"1": { "id": 1, "title": "Halo Dunia" },
"2": { "id": 2, "title": "Redux Toolkit" },
"3": { "id": 3, "title": "Normalisasi Data" }
}
}ids holds the order, entities holds the data. Updating item number 2 is just overwriting the entities["2"] entry — without touching any other item. Relations between entities also become easier, for example storing a userId array inside a post object instead of copying the entire user data.
createEntityAdapter accepts a selectId option for collections without an id field, and a sortComparer to set the default ordering:
import { createEntityAdapter } from "@reduxjs/toolkit"
interface Post {
id: number
title: string
createdAt: number
}
export const postsAdapter = createEntityAdapter<Post>({
selectId: (post) => post.id,
sortComparer: (a, b) => a.createdAt - b.createdAt,
})sortComparer determines the order of ids. Here a post with a smaller createdAt is placed first, so the list is automatically sorted by creation time. If it's omitted, the order follows insertion order.
The adapter provides ready-made reducers: setAll, addOne, addMany, upsertOne, upsertMany, updateOne, removeOne, and removeMany. All of them are used together with Immer inside reducers:
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
export const fetchPosts = createAsyncThunk(
"posts/fetchPosts",
async () => {
const res = await fetch("/api/posts")
return res.json()
},
)
const postsSlice = createSlice({
name: "posts",
initialState: postsAdapter.getInitialState({ status: "idle" }),
reducers: {
addPost: postsAdapter.addOne,
removePost: postsAdapter.removeOne,
removeManyPosts: postsAdapter.removeMany,
},
extraReducers: (builder) => {
builder.addCase(fetchPosts.fulfilled, (state, action) => {
postsAdapter.setAll(state, action.payload)
state.status = "succeeded"
})
},
})getInitialState creates an initial state containing empty ids, empty entities, plus the extra status field. The single line postsAdapter.setAll(state, action.payload) replaces all the manual logic for storing a fetch result.
The three most commonly used operations: upsertOne to insert a new item or overwrite an old one, addMany to add many items at once, and updateOne to modify an item by id:
import { createSlice } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
const postsSlice = createSlice({
name: "posts",
initialState: postsAdapter.getInitialState({ status: "idle" }),
reducers: {
upsertPost: postsAdapter.upsertOne,
updatePost: {
reducer: (state, action) => {
postsAdapter.updateOne(state, action.payload)
},
prepare: (id: number, changes: Partial<Post>) => ({
payload: { id, changes },
}),
},
clearPosts: postsAdapter.removeAll,
},
})updateOne accepts an { id, changes } object so the reducer stays one line. Every helper takes state as its first argument and returns the new result — no manual return needed.
For bulk operations like filtering a list, removeMany accepts an array of ids or a function predicate:
removeOldPosts: (state, action) => {
const cutoff = action.payload
postsAdapter.removeMany(state, (post) => post.createdAt < cutoff)
}The predicate is passed to removeMany as a callback that evaluates each entity. The adapter removes every item that satisfies the condition in a single state operation.
The adapter provides selectors that work against the slice state. To keep them type-safe, combine them with a root-level selector:
import { createSelector } from "@reduxjs/toolkit"
import { postsAdapter } from "./postsAdapter"
import type { RootState } from "../../app/store"
export const { selectAll, selectById, selectEntities, selectIds } =
postsAdapter.getSelectors((state: RootState) => state.posts)
export const selectPostById = (id: number) =>
createSelector(selectById, (entity) => entity?.[id])
export const selectRecentPosts = createSelector(
[selectAll],
(posts) => posts.filter((p) => p.createdAt > Date.now() - 86400000).slice(0, 5),
)getSelectors takes a slice-selecting function, then produces selectors that read directly from the root state. selectById essentially selects the entire entities; wrap it with createSelector when you want to pick out one specific item.
Tip
selectAll returns a new array every time it's called if you filter in the component. Compose it through createSelector like the example above so the result stays memoized and doesn't trigger excessive re-renders.
Normalizing with createEntityAdapter changes how we manage collections: state that was once an array full of manual calculations now becomes an ids + entities pair with one-line CRUD. Id-based operations are cheap, ordering is kept consistent through sortComparer, and built-in selectors keep data access in components concise.
Key takeaways:
ids for order and entities for the data dictionary.createEntityAdapter generates CRUD reducers: setAll, upsertOne, updateOne, removeMany.getInitialState can be given extra fields like status.getSelectors produces selectAll, selectById, selectEntities, and selectIds.sortComparer keeps the ids ordering consistent.createSelector for memoized derived data.In the next episode, episode 11 covers store structure & combineSlices — how to combine modular reducers, leverage lazy loading for code-splitting, and design healthy per-feature state shapes for large applications.