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

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
Practice | Why It's Important |
---|---|
Use createSlice | Keeps reducers clean |
Avoid persisting sensitive data | LocalStorage is not encrypted |
Use whitelist wisely | Only persist what’s needed |
Separate Redux logic by feature | Helps 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! 🚀