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;
layout.tsx
Provider Setup in This is where things usually break. You cannot use PersistGate
directly in layout.tsx
because it’s a server component.
ReduxProvider.tsx
(Client Component)
Create 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>
);
}
layout.tsx
Wrap it in 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 likelayout.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!