Share
1

Advanced Redux Patterns: Normalization, Reselect, and Persistence

by ObserverPoint · July 27, 2025

While Redux (especially with Redux Toolkit) provides a robust foundation for state management in React applications, building complex, performant, and user-friendly applications often requires delving into more advanced patterns. These patterns address common challenges such as managing complex data relationships, optimizing performance, and preserving user experience across sessions. This article will explore three essential advanced Redux patterns: Normalizing State, using Reselect for Optimized Selectors, and leveraging Redux Persist for State Persistence.


1. Normalizing State

In many real-world applications, data often comes from APIs with nested structures and duplicated information. Storing this data directly in your Redux store can lead to issues like:

  • Duplication: The same data (e.g., a user object) might appear in multiple places.
  • Difficulty in Updating: Updating a piece of data requires finding and updating it in all its nested locations.
  • Performance Issues: Deeply nested data can make updates slower and require more complex selectors.

Normalizing state means transforming your nested data into a flat, relational structure where each entity type (e.g., users, posts, comments) is stored in its own “table” (an object keyed by ID), and relationships between entities are represented by IDs.

1.1. Benefits of Normalizing State:

  • Single Source of Truth: Each entity exists in only one place, eliminating duplication.
  • Easier Updates: Updating an entity only requires modifying it in one location in the store.
  • Improved Performance: Flat structures are generally faster to access and update.
  • Simplified Selectors: Easier to retrieve specific entities by their ID.

1.2. Example: Unnormalized vs. Normalized State

Unnormalized State (Problematic):

{
  posts: [
    {
      id: "post1",
      title: "My First Post",
      author: { id: "userA", name: "Alice" },
      comments: [
        { id: "comm1", text: "Great post!", user: { id: "userB", name: "Bob" } },
        { id: "comm2", text: "Agreed.", user: { id: "userA", name: "Alice" } } // Alice duplicated
      ]
    },
    {
      id: "post2",
      title: "Another Post",
      author: { id: "userB", name: "Bob" }, // Bob duplicated
      comments: []
    }
  ]
}
    

Normalized State (Solution):

{
  posts: {
    byId: {
      "post1": { id: "post1", title: "My First Post", author: "userA", comments: ["comm1", "comm2"] },
      "post2": { id: "post2", title: "Another Post", author: "userB", comments: [] }
    },
    allIds: ["post1", "post2"]
  },
  users: {
    byId: {
      "userA": { id: "userA", name: "Alice" },
      "userB": { id: "userB", name: "Bob" }
    },
    allIds: ["userA", "userB"]
  },
  comments: {
    byId: {
      "comm1": { id: "comm1", text: "Great post!", user: "userB" },
      "comm2": { id: "comm2", text: "Agreed.", user: "userA" }
    },
    allIds: ["comm1", "comm2"]
  }
}
    

Tooling: Redux Toolkit’s `createEntityAdapter` is designed to simplify normalizing state and managing entities with common CRUD operations.


2. Reselect for Optimized Selectors

In Redux, “selectors” are functions that extract pieces of data from the Redux store. When components subscribe to the store, they often use selectors. A common performance pitfall is when selectors perform expensive computations (e.g., filtering, mapping, sorting data) every time the Redux state changes, even if the relevant slice of state hasn’t changed. This can lead to unnecessary re-renders of your React components.

Reselect is a library that creates “memoized selectors.” Memoization is a technique where the result of a function call is cached, and if the same inputs occur again, the cached result is returned instead of re-executing the function.

2.1. Benefits of Reselect:

  • Performance Optimization: Prevents re-computation of derived data unless relevant parts of the state have changed, reducing CPU usage.
  • Prevent Unnecessary Re-renders: By returning the same reference if the data hasn’t changed, it prevents React components from re-rendering when their props haven’t actually changed.
  • Composability: Selectors can be composed from other selectors, creating powerful and reusable data transformations.

2.2. How Reselect Works with `createSelector`:

`createSelector` takes one or more “input selectors” and a “result function.”

  • Input Selectors: Simple selectors that extract specific slices of state.
  • Result Function: A function that takes the outputs of the input selectors as arguments and performs the transformation.

The result function only runs if the inputs from the input selectors have changed. Otherwise, it returns the previously computed result.

2.3. Example with Reselect:

import { createSelector } from '@reduxjs/toolkit'; // Re-export from Reselect

// 1. Input Selectors (simple data extraction)
const selectUsers = state => state.users.byId;
const selectPosts = state => state.posts.byId;

// 2. Memoized Selector
// This selector will only re-run if `selectUsers` or `selectPosts` return new values
export const selectPostsWithAuthors = createSelector(
  [selectPosts, selectUsers], // Input selectors
  (posts, users) => { // Result function
    console.log('Calculating posts with authors...'); // This log will show less often
    return Object.values(posts).map(post => ({
      ...post,
      author: users[post.author] // Attach author details from normalized users
    }));
  }
);

// In a React component:
// const postsWithAuthors = useSelector(selectPostsWithAuthors);
// This will only re-calculate and cause a re-render if posts or users data actually changes.
    

3. Redux Persist for State Persistence

By default, the Redux store’s state is ephemeral: it resets to its initial state every time the user refreshes the browser or closes and reopens the application. For many applications (e.g., remembering login status, user preferences, items in a shopping cart), it’s crucial to preserve parts of the state across sessions.

Redux Persist is a library that allows you to save (persist) your Redux store state to a storage mechanism (like `localStorage` or `sessionStorage`) and rehydrate it when the application starts.

3.1. Benefits of Redux Persist:

  • Enhanced User Experience: Users don’t lose their session or preferences on refresh, providing a seamless experience.
  • Offline Capabilities: Can contribute to basic offline functionality by maintaining state without a network connection.
  • Simplified Development: Avoids the need for manual saving/loading of state to storage.

3.2. Setup and Configuration:

  1. Installation:
    npm install redux-persist

  2. Configure Persist: Wrap your root reducer with `persistReducer` and use `persistStore` to create the persistor.

Example (with Redux Toolkit):

// src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web

import authReducer from './features/auth/authSlice';
import cartReducer from './features/cart/cartSlice';
import uiReducer from './features/ui/uiSlice';

// Configuration for redux-persist
const persistConfig = {
  key: 'root', // Key for the persisted state in storage
  storage, // Which storage to use (localStorage, sessionStorage, etc.)
  whitelist: ['auth', 'cart'], // Only these reducers will be persisted
  // blacklist: ['ui'] // These reducers will NOT be persisted
};

// Combine all reducers
const rootReducer = combineReducers({
  auth: authReducer,
  cart: cartReducer,
  ui: uiReducer,
});

// Create a persisted reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
});

export const persistor = persistStore(store);
    
  1. Integrate with React App: Wrap your `App` component with `PersistGate`.
// src/index.js (or App.js)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import App from './App';
import { store, persistor } from './store'; // Import store and persistor

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}> { /* Loading can be a spinner */ }
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);
    

By implementing state normalization, optimizing selectors with Reselect, and persisting state with Redux Persist, you can build highly performant, scalable, and user-friendly React applications that leverage Redux to its full potential.


References

You may also like