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;
47Step 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}
16Example 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-logger1import 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
When Redux Toolkit + redux-persist Truly Shines
Once your application moves beyond a few pages, global state becomes unavoidable. Authentication status, user preferences, cart data, theme settings these are things users expect to persist and behave consistently.
This is where Redux Toolkit combined with redux-persist stops being “just another setup” and becomes a real productivity booster.
Thinking Beyond Page Reloads
From a user’s perspective, losing state feels like a bug.
Imagine:
- A logged-in user refreshing the page and getting logged out
- A shopping cart resetting unexpectedly
- App preferences reverting to default
Persisted state solves these problems quietly in the background, improving trust and usability without adding visible complexity to your UI.
Scaling Redux in Real Applications
As your project grows, Redux often evolves into a central data layer.
In real-world Next.js apps, teams usually:
- Keep persisted state minimal and intentional
- Separate UI state from business-critical state
- Avoid persisting large or frequently changing data
- Use feature-based slices for better maintainability
This approach keeps your app fast and avoids unnecessary storage bloat.
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! 🚀

