Dev By Diwash
How to Use Redux with Persistence in Next.js Image

Spread it online!

How to use Redux in Next.js

Redux is still one of the most powerful tools for managing global state — but integrating it with the App Router in Next.js 13+ can cause issues if done incorrectly.

If you're facing an error like:

TypeError: Super expression must either be null or a function

You're not alone.

This blog covers how to use Redux (with Redux Toolkit and Redux Persist) correctly in Next.js (App Router) and avoid SSR issues.

This guide is for you.

What You'll Learn

  • How to set up Redux Toolkit with Redux Persist
  • How to fix SSR compatibility issues in Next.js App Router
  • Best folder structure for global state management
  • Clean integration using client/server separation

Why This Error Happens

The error occurs because redux-persist uses PersistGate, which is not compatible with Server Components. Next.js App Router now uses server components by default, so PersistGate must only run on the client.

Key Fixes

  • Move Redux logic into a dedicated redux-provider.tsx component
  • Mark it with "use client" at the top
  • Wrap your layout or root component with this provider

Install Dependencies

Install the required dependencies to set up Redux with persistence.

# Using NPM
npm install @reduxjs/toolkit react-redux redux-persist
 
# Using Yarn
yarn add @reduxjs/toolkit react-redux redux-persist
 
# Using PNPM
pnpm add @reduxjs/toolkit react-redux redux-persist
 
# Using Bun
bun add @reduxjs/toolkit react-redux redux-persist

Folder Structure

Organize your Redux setup on Next.js like this:

src/
├── app/
   └── layout.tsx
├── components/
   └── redux-provider.tsx
├── hooks/
   └── use-typed-selectors.ts
├── store/
   ├── index.ts
   └── features/
       └── app.ts

Store Setup

We’ll configure Redux Toolkit and Redux Persist together for session persistence.

// src/store/index.ts
 
import appReducer from "./features/app";
import { persistReducer, persistStore } from "redux-persist";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import createWebStorage from "redux-persist/es/storage/createWebStorage";
 
const createNoopStorage = () => {
  return {
    getItem(_key: string) {
      return Promise.resolve(null);
    },
    setItem(_key: string, value: any) {
      return Promise.resolve(value);
    },
    removeItem(_key: string) {
      return Promise.resolve();
    },
  };
};
 
const storage =
  typeof window !== "undefined"
    ? createWebStorage("local")
    : createNoopStorage();
 
const persistConfig = {
  key: "root",
  storage,
  version: 1,
};
 
const rootReducer = combineReducers({
  app: appReducer,
});
 
const persistedReducer = persistReducer(persistConfig, rootReducer);
 
export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});
 
export const persistor = persistStore(store);
 
export type RootAppState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

Create a Slice

Define your initial state and actions using createSlice.

// src/store/features/app.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
 
interface AppState {
  count: number;
}
 
const initialState: AppState = {
  count: 0,
};
 
const appSlice = createSlice({
  name: "app",
  initialState,
  reducers: {
    increment: (state) => {
      state.count += 1;
    },
    decrement: (state) => {
      state.count -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.count += action.payload;
    },
  },
});
 
export const { increment, decrement, incrementByAmount } = appSlice.actions;
export default appSlice.reducer;

Create Typed Hooks

To enhance type safety and avoid repetitive typing, define custom hooks.

// /src/hooks/use-typed-selectors.ts
 
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import { AppDispatch, RootAppState } from "../store";
 
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootAppState> = useSelector;

Provider Setup in layout.tsx

This is where things usually break. You cannot use PersistGate directly in layout.tsx because it’s a server component.

Create ReduxProvider.tsx (Client Component)

We’ll make a client-only component that wraps Redux and PersistGate.

// src/components/redux-provider.tsx
 
"use client";
 
import { Provider } from "react-redux";
import { store, persistor } from "@/store";
import { PersistGate } from "redux-persist/integration/react";
 
export default function ReduxProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        {children}
      </PersistGate>
    </Provider>
  );
}

Wrap it in layout.tsx

Use this wrapper to hydrate Redux properly in the app shell.

import "./globals.css";
 
import type { Metadata } from "next";
import ReduxProvider from "@/components/redux-provider";
 
export const metadata: Metadata = {
  title: "How to use Redux in Next.js",
  description: "Learn how to use Redux in Next.js",
};
 
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <ReduxProvider>{children}</ReduxProvider>
      </body>
    </html>
  );
}

Use Redux in Components

Use the typed hooks (useAppDispatch and useAppSelector) in any client component.

"use client";
 
import { RootAppState } from "@/store";
import { increment, decrement } from "@/store/features/app";
import { useAppDispatch, useAppSelector } from "@/hooks/use-typed-selectors";
 
const Counter = () => {
  const { count } = useAppSelector((state: RootAppState) => state.app);
  const dispatch = useAppDispatch();
 
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
};
 
export default Counter;

Common Pitfalls

  • Using PersistGate in server components like layout.tsx
  • Not marking ReduxProvider with "use client"
  • Using outdated Redux syntax or missing combineReducers

Redux is still one of the most powerful tools for managing global state — and with this setup, it's fully compatible with Next.js 14 App Router, persistence, and modern SSR requirements.

Let me know on LinkedIn if this helped you, or drop your questions!