This episode covers Redux performance: reselect and createSelector for memoization, composing derived state, avoiding excessive re-renders with shallowEqual, splitting granular state, and profiling techniques using Redux DevTools.

Redux offers good performance by default, but how you write selectors determines how often components re-render. A selector that returns a new array or object on every call makes useSelector think the state changed — even when the data is identical. Episode 13 covers how to control this with reselect and createSelector.
The goal is simple: make sure components only render when the data they read actually changes. We'll learn memoization, selector composition, shallow equality, granular state structure, and how to verify all of it with Redux DevTools.
Look at a selector that filters on every call:
import { createSelector } from "@reduxjs/toolkit"
export const selectVisiblePosts = createSelector(
[(state) => state.posts.items],
(items) => items.filter((post) => post.published),
)Without createSelector, the items.filter(...) code produces a new array on every call. useSelector compares results with ===; because the reference differs, the component re-renders on every action — even actions that don't touch posts at all.
createSelector remembers the last inputs and its result. It only evaluates the combination function when one of the input selectors returns a different value:
import { createSelector } from "@reduxjs/toolkit"
const selectItems = (state) => state.posts.items
const selectKeyword = (state) => state.posts.keyword
export const selectFilteredPosts = createSelector(
[selectItems, selectKeyword],
(items, keyword) =>
items.filter((post) => post.title.toLowerCase().includes(keyword)),
)As long as items and keyword don't change, the filter result is cached and reused by reference. A component using selectFilteredPosts only re-renders when the keyword or the items actually change.
Memoized selectors can be composed in layers. The result of one selector becomes the input of the next, so expensive computations aren't repeated:
import { createSelector } from "@reduxjs/toolkit"
const selectAllUsers = (state) => state.users.items
const selectActiveUserId = (state) => state.users.activeId
export const selectActiveUser = createSelector(
[selectAllUsers, selectActiveUserId],
(users, activeId) => users.find((u) => u.id === activeId),
)
export const selectActiveUserStats = createSelector(
[selectActiveUser],
(user) => {
if (!user) return null
return { postCount: user.postIds.length, followerCount: user.followers }
},
)selectActiveUserStats is only recomputed when selectActiveUser returns a new reference — that is, when activeId or the users list changes. This composition keeps performance linear with the amount of data that actually changes.
The bigger the slice a selector pulls, the more components re-render with it. Select only the fields the component needs:
import { useAppSelector } from "../../app/hooks"
export function UserName() {
const name = useAppSelector((state) => state.users.items[0]?.name)
return <p>{name}</p>
}useAppSelector compares results with ===. Returning state.users.items[0]?.name (a string) means the component only re-renders when the first user's name changes — not every time other users are added.
Sometimes a selector must return an object or array assembled from several fields. shallowEqual compares one level of properties so a change in one key doesn't trigger a re-render just because the object is new:
import { useAppSelector } from "../../app/hooks"
import { shallowEqual } from "react-redux"
export function ProfileHeader() {
const profile = useAppSelector(
(state) => ({
name: state.users.items[0]?.name,
avatar: state.users.items[0]?.avatar,
}),
shallowEqual,
)
return <header>{profile.name}</header>
}Without shallowEqual, a new object on every call makes the component render constantly. With shallowEqual, the component only re-renders when name or avatar actually changes value.
Designing granular state helps selectors work more efficiently. Keep data that changes at different frequencies in separate keys:
{
"posts": {
"items": [ ... ],
"editingId": null,
"filters": { "keyword": "", "sortBy": "newest" }
}
}Typing in the search box only changes filters.keyword, so selectors that read posts.items keep using their memoized result. Keeping all data in one big array would mark the whole array as changed on every update.
Redux DevTools provides useful information for finding the source of re-renders:
npm view @reduxjs/toolkit versionPay attention to three things in the Action tab:
To verify the results of your optimization, log inside the render function:
export function PostList() {
const posts = useAppSelector(selectFilteredPosts)
console.log("PostList render", posts.length)
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}If the console.log fires when an irrelevant action is dispatched, your selectors aren't memoized yet. If it only fires when the read data changes, the optimization worked.
Warning
Memoization isn't magic: the cached result is reset every time an input changes, and createSelector still calls the input selectors on every evaluation. Don't recompute something heavy inside the input selectors themselves.
Redux performance is mostly determined by selectors. createSelector provides memoization for derived state, selector composition prevents repeated computation, shallowEqual handles partially-read collections, and granular state reduces the scope of re-renders. With profiles read from Redux DevTools, you can prove that every optimization actually works.
Key takeaways:
createSelector caches results until one of the input selectors changes.shallowEqual compares one-level properties for objects and arrays.In the next episode, episode 14 takes Redux to server rendering — you'll create a per-request store in the Next.js App Router, use Provider for SSR, hydrate state, and integrate RTK Query with Server Components.