How to Optimize Redux Selectors in React
Optimizing Redux selectors is crucial for maintaining high performance in React applications by preventing unnecessary component re-renders. This article covers the essential strategies for optimizing selectors, including utilizing memoization via Reselect, structuring normalized state, managing dynamic selector arguments, and avoiding common inline selector pitfalls.
1. Use Memoized Selectors with Reselect
By default, the useSelector hook runs its selector
function every time an action is dispatched. If your selector performs
computations and returns a new object reference (such as using
.filter() or .map()), React will re-render the
component even if the underlying data has not changed.
To prevent this, use createSelector from
@reduxjs/toolkit (which integrates the Reselect library).
This function memoizes the result, meaning it will only recalculate the
output if the input state slices change.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state) => state.items;
// This selector is memoized and will not cause re-renders if items remain unchanged
export const selectCompletedItems = createSelector(
[selectItems],
(items) => items.filter(item => item.completed)
);2. Avoid Inline Selectors in useSelector
Writing selector functions inline inside the useSelector
hook is a common mistake. Doing so creates a new function reference on
every render and bypasses memoization benefits if you are performing
calculations.
Inefficient:
// This runs and returns a new array reference on every dispatch
const completedItems = useSelector(state => state.items.filter(item => item.completed));Optimized:
// Pre-defined memoized selector used within the hook
const completedItems = useSelector(selectCompletedItems);3. Handle Dynamic Arguments Properly
When passing dynamic arguments to a selector (for example, looking up an item by its ID), sharing a single memoized selector across multiple components can break the cache because the arguments keep changing.
To optimize this, create a selector factory function. This ensures that each component instance gets its own private, memoized selector.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state) => state.items;
const selectItemId = (state, itemId) => itemId;
// Factory function for unique selector instances
export const makeSelectItemById = () =>
createSelector(
[selectItems, selectItemId],
(items, itemId) => items.find(item => item.id === itemId)
);Inside your component, initialize the selector once using
useMemo:
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { makeSelectItemById } from './selectors';
const ItemComponent = ({ itemId }) => {
const selectItemById = useMemo(makeSelectItemById, []);
const item = useSelector(state => selectItemById(state, itemId));
return <div>{item?.name}</div>;
};4. Keep State Normalized
Deeply nested state structures make selectors complex and slow. Normalizing your Redux state—storing data as an object keyed by ID instead of an array—simplifies selector logic and speeds up lookups to \(O(1)\) time complexity.
Instead of nested arrays, structure your state like this:
{
posts: {
byId: {
"post1": { id: "post1", title: "First Post" }
},
allIds: ["post1"]
}
}You can use Redux Toolkit’s createEntityAdapter to
automatically format, normalize, and generate optimized CRUD selectors
for your state.