How to Optimize Redux in React

Managing global state efficiently is crucial for maintaining high-performance React applications. While Redux offers a robust architecture for state management, improper configuration can lead to unnecessary component re-renders, sluggish UI transitions, and bloated bundle sizes. This guide outlines the most effective, actionable strategies to optimize Redux in React, focusing on state normalization, selector memoization, granular updates, and modern Redux Toolkit best practices.

1. Use Redux Toolkit (RTK)

Redux Toolkit is the official, opinionated toolset for efficient Redux development. It automatically configures the store with sensible defaults, including Redux Thunk and the Redux DevTools Extension. RTK uses Immer under the hood, allowing you to write mutable-style code that safely translates to immutable updates. This eliminates common bugs associated with manual state copying and reduces boilerplate code.

2. Memoize Selectors with Reselect

In React-Redux, the useSelector hook runs every time an action is dispatched. If your selector performs expensive calculations or returns a new object reference, it will force the component to re-render even if the underlying data has not changed.

To prevent this, use createSelector from Redux Toolkit (which integrates Reselect). Memoized selectors cache their inputs and outputs; they only recalculate when the specific slice of state they depend on actually changes.

import { createSelector } from '@reduxjs/toolkit';

const selectItems = (state) => state.items.list;
const selectFilter = (state) => state.items.filter;

// This selector only recalculates when list or filter changes
export const selectFilteredItems = createSelector(
  [selectItems, selectFilter],
  (list, filter) => list.filter(item => item.category === filter)
);

3. Keep useSelector Queries Granular

Components should only subscribe to the exact data they need to render. Instead of selecting an entire state object, select primitive values or specific properties.

If a component grabs a large object from the store, it will re-render whenever any property on that object changes, even if the component doesn’t use that specific property. If you must select an object, pass shallowEqual as the second argument to useSelector to perform a shallow comparison of the object properties instead of a strict reference check.

import { useSelector, shallowEqual } from 'react-redux';

// Better: Selects only the primitive value
const username = useSelector((state) => state.user.profile.username);

// Alternative: Use shallowEqual if selecting an object
const userProfile = useSelector((state) => state.user.profile, shallowEqual);

4. Normalize the State Structure

Deeply nested state trees make selectors complex and force frequent updates to unrelated components. Normalizing state means storing database-like tables of data using IDs as keys.

Instead of nesting comments inside posts, store posts and comments as separate slices, referencing each other by ID. Redux Toolkit’s createEntityAdapter is specifically designed to help normalize state easily, providing pre-built reducers and selectors for CRUD operations.

5. Leverage RTK Query for Data Fetching

A significant portion of Redux state is often used for caching server data. Implementing this manually requires writing numerous actions, reducers, and thunks for loading, success, and error states.

RTK Query (included with Redux Toolkit) automates data fetching, caching, synchronization, and polling. It eliminates the need for manual loading state tracking and automatically cleans up cached data when components unmount, drastically reducing memory usage and boilerplate.

6. Batch Actions to Minimize Renders

When dispatching multiple actions in response to a single event, React-Redux may trigger multiple re-renders. While React 18 batches state updates automatically in most scenarios, you can manually wrap consecutive dispatches in batch from react-redux to ensure only a single render pass occurs in older environments or complex asynchronous flows.

import { batch } from 'react-redux';

const handleReset = () => {
  batch(() => {
    dispatch(resetUser());
    dispatch(clearCart());
  });
};

7. Avoid Storing Non-Serializable Data

Do not store non-serializable values—such as Promises, functions, or complex class instances—in the Redux store. Storing non-serializable data breaks time-travel debugging, complicates state persistence, and degrades the performance of the Redux DevTools middleware. Keep the store strictly populated with plain JavaScript objects, arrays, and primitives.