This article guides you through integrating Redux Toolkit and Redux Persist in a React application created using Next.js 14’s App Router approach.

Next.js: A Secure and Streamlined Choice

For new React projects, the recommended approach is to use Next.js instead of the legacy npx create-react-app . Next.js offers numerous benefits, including security features and a framework tailored specifically for React development.

Challenge: Server-Side Local Storage with Redux Persist

Traditional Redux Persist implementations attempt to create local storage on the server-side. However, this is not possible in Next.js due to the lack of a window object in the server environment. The general error you see in terminal is redux-persist failed to create sync storage., falling back to Noop storage Consequently, these implementations may fail to update local storage as the state changes.

Solution for Next.js App Router:

This guide provides a solution that addresses the server-side limitations of traditional Redux Persist implementations, ensuring that local storage is updated correctly in your Next.js application.

By the date Iam writing this article., Iam using React 18.2.0 and Next 14.1.0 version which is latest and uses App Router which is recommended approach by Next.js boiler plate setup.

Note: iam using https://redux-toolkit.js.org/usage/nextjs to setup my redux toolkit with next.js

Step 1: folder structure of your React app with Next.js 14 and App Router

project-name/
├── public/    # Static assets (images, fonts, etc.)
├── styles/    # Global and component-specific styles
└── src/       # Source code
    ├── app/     # App directory for server-rendered components
    ├── layout.tsx     # wrapper component to load app pages as children
    │   ├── api/  # Optional: API routes defined within app
    │   └── lib/  # To store all redux configuration
    │       ├── store.ts       # To create store
    │       ├── RootReducer.ts   # To set reducers
    │       └── hooks.ts        # to use redux state
    ├── components/  # Reusable UI components
    │   ├── Button.tsx
    │   └── ...
    ├── hooks/      # Custom React hooks
    │   ├── useFetchData.tsx
    │   └── ...
    ├── utils/      # Utility functions and helpers
    │   ├── api.ts
    │   └── ...
    ├── slices/     # Redux Toolkit setup (alternative to the previous structure)
    │   ├── rootReducer.ts
    │   ├── slice1.ts
    │   └── ...
    └── package.json        # Dependencies and scripts
    └── ...                # Other files/folders as needed

Step 2: Create store.ts as follows.

// Import necessary libraries
import { configureStore } from "@reduxjs/toolkit";
import RootReducer from "./RootReducer"; // Replace with the path to your root reducer file
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist";
import storage from "redux-persist/lib/storage"; // Use browser local storage for persistence

// Define the persistence configuration
const persistConfig = {
  key: "root", // Key under which the persisted state will be stored
  storage, // Use local storage as the storage mechanism
};

// Create a persisted reducer using the RootReducer and persistConfig
const persistedReducer = persistReducer(persistConfig, RootReducer);

// Function to create a configured store based on RootReducer
const makeConfiguredStore = () =>
  configureStore({
    reducer: RootReducer, // Set the reducer to the RootReducer
  });

// Function to create a store with optional persistence based on server/client environment
export const makeStore = () => {
  // Check if the code is running on the server (no window object)
  const isServer = typeof window === "undefined";

  // If on the server, create a store without persistence
  if (isServer) {
    return makeConfiguredStore();
  }

  // If on the client, create a persisted reducer and store
  const persistedReducer = persistReducer(persistConfig, RootReducer);
  let store = configureStore({
    reducer: persistedReducer, // Set the reducer to the persistedReducer
  });

  // Persist the store using the persisted reducer and store it in the store object
  store.__persistor = persistStore(store);

  // Return the store with the persisted store attached
  return store;
};

// Define types for AppStore, RootState, and AppDispatch based on the store
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

// Persist the store created by makeStore in a separate variable
export const persistor = persistStore(makeStore());

Step 3: Set your Root Reducer as follows:

// Import the combineReducers function from @reduxjs/toolkit
import { combineReducers } from "@reduxjs/toolkit";

// Import the SliceReducer from the slices folder (replace with your actual path)
import SliceReducer from "@/slices/mySlice"; // Assuming `mySlice` is the slice file name

// Define a constant for the logout action type
export const LOGOUT = "LOGOUT";

// Create a combined reducer using combineReducers
const appReducer = combineReducers({
  MySlice: SliceReducer,
  // Add other reducers here following the same format: `SliceName: sliceReducer`
});

// Define the root reducer function
const RootReducer = (state, action) => {
  // Handle the LOGOUT action type specially
  if (action.type === LOGOUT) {
    // Reset the state to undefined for all reducers on logout
    return appReducer(undefined, action);
  }

  // Otherwise, delegate handling to the combined reducer
  return appReducer(state, action);
};

// Export the RootReducer as the default export
export default RootReducer;

Step 4: Setup hooks to get redux data in our pages/components

// Import necessary hooks from react-redux
import { useDispatch, useSelector, useStore } from "react-redux";

// Import type definitions from your store
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch, AppStore } from "./store";

// Define a custom hook for dispatching actions: useAppDispatch
export const useAppDispatch: () => AppDispatch = useDispatch;

// This hook uses the built-in `useDispatch` hook but casts the return value to the specific `AppDispatch` type for better type safety.
// It allows you to dispatch actions to the store within your components.

// Define a custom hook for selecting state: useAppSelector
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// This hook uses the built-in `useSelector` hook but provides the generic type `TypedUseSelectorHook<RootState>` for type safety.
// It allows you to access the Redux state within your components.

// Define a custom hook for accessing the store: useAppStore
export const useAppStore: () => AppStore = useStore;

// This hook uses the built-in `useStore` hook but casts the return value to the specific `AppStore` type.
// It allows you to access the entire Redux store object within your components, although using `useSelector` is usually preferred for accessing specific parts of the state.

Step 5: Setting up layout.tsx which acts as wrapper for app children pages

import { useRef } from "react";
import "./globals.css"; // Import global styles
import Header from "./components/Header"; // Import the Header component
import { usePathname } from "next/navigation"; // Get pathname from Next.js
import { Box, Grid } from "@mui/material"; // Import components from Material UI
import { AppStore, makeStore, persistor } from "@/lib/store"; // Import store and persistor
import { Provider } from "react-redux"; // Provider for Redux store
import { PersistGate } from "redux-persist/integration/react"; // PersistGate for persisted store

export default function RootLayout({ children }: { children: React.ReactNode }) {
  // Get the current pathname using the usePathname hook
  const pathName = usePathname();

  // Create a ref to store the Redux store instance
  const storeRef = useRef<AppStore>();

  // Check if the store is already created in the ref
  if (!storeRef.current) {
    // If not, create a new store using makeStore
    storeRef.current = makeStore();
  }

  return (
    <html lang="en">
      <body>
        {/* Wrap the application with Redux Provider */}
        <Provider store={storeRef.current}>
          {/* PersistGate for persisted state with null loading indicator */}
          <PersistGate loading={null} persistor={storeRef.current.__persistor}>
            {/* Main layout structure */}
            <Box sx={{ display: "flex" }}>
              {/* Conditionally render Header based on pathname (except root) */}
              {pathName !== "/" && <Header />}
              {/* Render the provided children components */}
              {children}
            </Box>
          </PersistGate>
        </Provider>
      </body>
    </html>
  );
}

Step 6: Usage of hooks:

import React, { useState } from "react";
import { setMySliceData } from "@/redux/slices/MySlice"; // Assuming the correct path
import { useAppDispatch, useAppSelector } from "@/lib/hooks";

const Component: React.FC = () => {
  // Use the useAppDispatch hook to dispatch actions
  const dispatch = useAppDispatch();

  // You can still access the store state using useAppSelector if needed
  const state = useAppSelector((state) => state.MySlice.data); // Assuming the data is in MySlice.data

  // Rest of your code can use dispatch and state
  dispatch(setMySliceData(stateData)); // Dispatch the action using the dispatch hook

  return (
    // Your component code goes here
  );
};

export default Component;

and we are ready for react app ready with redux-persist.