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.