storieasy-logo

Mastering State Persistence in Next.js: Redux Toolkit & redux-persist Integration

avatar

Info

23 May 2025

|

15 min to read

how-integration-redux-persist-in-next.js

redux-persist

redux

next.js

redux toolkit with next.js

redux persist in next.js

state management

In the world of React frameworks, Next.js stands tall with features like hybrid rendering, route pre-fetching, and file-based routing. However, as applications scale, state management becomes a central concern. That’s where Redux Toolkit steps in—modern, efficient, and powerful. Combine that with redux-persist, and you have a solid setup for handling and persisting global state in your Next.js app.

🧠 What You'll Learn

  • Why Redux Toolkit and redux-persist matter
  • Basic setup in a Next.js app
  • Making it Next.js-compatible (SSR & hydration-safe)
  • Best practices for folder structure
  • Advanced tips (middleware, blacklist/whitelist, debug tools)

🔍 Why Redux Toolkit + redux-persist?

❗ Redux Toolkit:

  • Eliminates boilerplate
  • Comes with createSlice, configureStore, and built-in devtools
  • Great for code splitting and modular structure

💾 redux-persist:

  • Saves your Redux state to storage (usually localStorage)
  • Restores state even after a page reload
  • Works great with Next.js when proper hydration strategies are followed

🚀 Setting Up a Next.js App

If you haven't already:

1npx create-next-app@latest nextjs-redux-app
2cd nextjs-redux-app
3npm install @reduxjs/toolkit react-redux redux-persist

🧩 Step 1: Creating a Redux Slice

Create a file src/store/slices/books.ts:

1import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2
3type BookStatus = "published" | "request" | "out of stock";
4
5interface Book {
6  id: number;
7  bookName: string;
8  status: BookStatus;
9  author: string;
10  price: number;
11}
12
13const initialState: Book[] = [];
14
15const booksSlice = createSlice({
16  name: "books",
17  initialState,
18  reducers: {
19    // Add book with "request" status
20    addRequestToAdmin: (state, action) => {
21      state.push({
22        id: Date.now(),
23        bookName: action.payload.bookName,
24        author: action.payload.author,
25        price: action.payload.price,
26        status: "request",
27      });
28    },
29
30    updateBookStatus: (state, action) => {
31      const { id, status } = action.payload;
32      const book = state.find((b) => b.id === id);
33      if (book) {
34        book.status = status;
35      }
36    },
37
38    deleteBook: (state, action: PayloadAction<{ id: number }>) => {
39      return state.filter((book) => book.id !== action.payload.id);
40    },
41  },
42});
43
44export const { addRequestToAdmin, updateBookStatus, deleteBook } =
45  booksSlice.actions;
46export default booksSlice.reducer;
47

🏗️ Step 2: Configure Store with redux-persist

Create src/store/store.ts:

1import booksReducer from "./slices/books";
2import { configureStore } from "@reduxjs/toolkit";
3import { combineReducers } from "redux";
4import storage from "redux-persist/lib/storage"; // defaults to localStorage for web
5import { persistReducer, persistStore } from "redux-persist";
6
7const rootReducer = combineReducers({
8  books: booksReducer,
9});
10
11const persistConfig = {
12  key: "root",
13  storage,
14  whitelist: ["books"], // specify which reducers to persist
15};
16
17const persistedReducer = persistReducer(persistConfig, rootReducer);
18
19export const store = configureStore({
20  reducer: persistedReducer,
21});
22
23export const persistor = persistStore(store);
24
25export type RootState = ReturnType<typeof store.getState>;
26export type AppDispatch = typeof store.dispatch;
27

🔌 Step 3: Integrate Redux with Next.js App

Edit src/app/layout.tsx:

1import type { Metadata } from "next";
2import { Geist, Geist_Mono } from "next/font/google";
3import "./globals.css";
4import Providers from "./providers";
5
6const geistSans = Geist({
7  variable: "--font-geist-sans",
8  subsets: ["latin"],
9});
10
11const geistMono = Geist_Mono({
12  variable: "--font-geist-mono",
13  subsets: ["latin"],
14});
15
16export const metadata: Metadata = {
17  title: "Create Next App",
18  description: "Generated by create next app",
19};
20
21export default function RootLayout({
22  children,
23}: Readonly<{
24  children: React.ReactNode;
25}>) {
26  return (
27    <html lang="en">
28      <body
29        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30      >
31        <Providers>{children}</Providers>
32      </body>
33    </html>
34  );
35}

Create src/app/providers.tsx:

1"use client";
2import { ReactNode } from "react";
3import { Provider } from "react-redux";
4import { persistor, store } from "@/store/store";
5import { PersistGate } from "redux-persist/integration/react";
6
7export default function Providers({ children }: { children: ReactNode }) {
8  return (
9    <Provider store={store}>
10      <PersistGate loading={null} persistor={persistor}>
11        {children}
12      </PersistGate>
13    </Provider>
14  );
15}
16

📦 Example Usage in Component

useSelector to get Data and useDispatch to set data

1// components/BooksTable.js
2import { deleteBook, updateBookStatus } from "@/store/slices/books";
3import { RootState } from "@/store/store";
4import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/outline";
5import { useDispatch, useSelector } from "react-redux";
6
7type BookStatus = "published" | "request" | "out of stock";
8
9const statusColors: Record<BookStatus, string> = {
10  published: "bg-green-600",
11  request: "bg-yellow-600",
12  "out of stock": "bg-red-600",
13};
14
15export default function AdminTable() {
16  const dispatch = useDispatch();
17  const books = useSelector((state: RootState) => state.books);
18
19  console.log("books", books);
20
21  const handleStatusChange = (id: number, newStatus: string) => {
22    dispatch(updateBookStatus({ id, status: newStatus }));
23  };
24
25  return (
26    <div className="bg-gray-900 text-white p-6 rounded-lg shadow-md">
27      ...
28    </div>
29  );
30}

⚙️ Advanced: Middleware, Debugging, SSR Notes

✅ Middleware

You can add middleware like redux-logger:

1npm install redux-logger
1import logger from 'redux-logger';
2middleware: (getDefaultMiddleware) =>
3  getDefaultMiddleware({ serializableCheck: false }).concat(logger),

❗ SSR Compatibility Tips

Redux-persist uses localStorage, which doesn’t exist on the server. So you should:

  • Ensure persistence is only applied on the client
  • Use hydration guards in custom hooks or layout logic

⚠️ Avoid hydration mismatch

Sometimes Redux state differs between server and client on initial load. A good practice is to:

  • Wrap sensitive state-rendering logic in useEffect
  • Use placeholders during SSR

🧼 Best Practices

PracticeWhy It's Important
Use createSlice Keeps reducers clean
Avoid persisting sensitive dataLocalStorage is not encrypted
Use whitelist wiselyOnly persist what’s needed
Separate Redux logic by featureHelps scale your app

🎯 Conclusion

Integrating Redux Toolkit with redux-persist in a Next.js application offers the perfect combination of robust state management and persistent global state—while remaining aligned with React's modern best practices and the architectural strengths of Next.js.

By following this step-by-step guide, you're not just configuring Redux; you're laying the foundation for a scalable, maintainable, and production-ready application. Whether you're handling complex user flows, persisting user preferences, or preparing for server-side rendering (SSR), this setup ensures your app is ready for real-world performance challenges.

For a complete working example and hands-on implementation, feel free to explore my GitHub repository here:
🔗 https://github.com/dhruveshborad/nextJs-redux

Happy coding, and may your state always be in sync! 🚀

Newsletter

Subscribe to our newsletter and get our latest updates.

Share with your friends: