When working with global state management in Next.js 15, integrating Redux efficiently is crucial. Instead of directly wrapping the entire app inside Provider
, it's best to create a dedicated provider component that manages Redux (and other global providers). This ensures better modularity, scalability, and cleaner architecture.
In this blog, we’ll set up Redux in Next.js 15 with a structured provider approach.
1️⃣ Creating the Root Layout (RootLayout.tsx
)
The RootLayout.tsx
file is the entry point for our Next.js application. Here, we wrap the app inside a custom MainProvider
, which will hold all global providers (Redux, Auth, Theme, etc.).
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className="dark" style={{ colorScheme: "dark" }}>
<body className="antialiased w-full min-h-screen overflow-x-hidden">
<MainProvider>{children}</MainProvider>
</body>
</html>
);
}
💡 Why use
MainProvider
?This approach ensures that all global providers are separated from the layout, making it easier to manage and extend.
2️⃣ Creating MainProvider.tsx
The MainProvider
component serves as the central place to wrap all providers. Currently, it only includes ReduxProvider
, but you can add Auth, Theme, or other providers later.
"use client";
import ReduxProvider from "./ReduxProvider";
const MainProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <ReduxProvider>{children}</ReduxProvider>;
};
export default MainProvider;
🚀 Modular Architecture
This approach ensures we can extend providers easily in the future without modifying
RootLayout.tsx
.
3️⃣ Setting Up ReduxProvider.tsx
Next, we create ReduxProvider.tsx
, which initializes Redux and ensures the store remains persistent.
"use client";
import { useRef } from "react";
import { Provider } from "react-redux";
import { store, AppStore } from "@/lib/redux/store";
const ReduxProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const storeRef = useRef<AppStore | null>(null);
if (!storeRef.current) {
storeRef.current = store();
}
return <Provider store={storeRef.current}>{children}</Provider>;
};
export default ReduxProvider;
✅ Why
useRef
?We use
useRef
to prevent reinitializing the Redux store on every render, ensuring better performance.
4️⃣ Setting Up Redux Store (store.ts
)
Now, let’s configure our Redux store inside lib/redux/store.ts
.
import { configureStore } from "@reduxjs/toolkit";
import favoriteReducer from "./features/favorite/favoriteSlice";
export const store = () => {
return configureStore({
reducer: {
favorite: favoriteReducer,
},
});
};
export type AppStore = ReturnType<typeof store>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];
🔥 Dynamic Store Initialization
Instead of exporting a singleton store, we use a factory function (
store()
), which allows Next.js server actions and middleware integration.
5️⃣ Creating Hooks for Redux (redux.hooks.ts
)
Instead of importing useDispatch
and useSelector
directly, let’s create typed Redux hooks.
import { useDispatch, useSelector, useStore } from "react-redux";
import type { RootState, AppDispatch, AppStore } from "@/lib/redux/store";
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppStore = useStore.withTypes<AppStore>();
🛠 Typed Redux Hooks
This ensures better TypeScript support and avoids repetitive type definitions.
6️⃣ Creating a Redux Slice (favoriteSlice.ts
)
Let’s create a Redux slice for handling favorite items inside features/favorite/favoriteSlice.ts
.
import { createSlice, nanoid, type PayloadAction } from "@reduxjs/toolkit";
import { useAppSelector } from "@/hooks/redux.hooks";
interface FavoriteProductType {}
interface InitialStateType = {
favoriteProduct: FavoriteProductType[]
};
const initialState: InitialStateType = {};
const favoriteSlice = createSlice({
name: "favorites",
initialState,
reducers: {
addFavorite: (state, action: PayloadAction<FavoriteProductType>) => {}
},
});
export const useFavoriteProduct = () =>
useAppSelector((state) => state.favorite);
export const { addFavorite } = favoriteSlice.actions;
export default favoriteSlice.reducer;
🛒 Scalable State Management
Using Redux slices ensures better modularity when adding new features.
Conclusion
By following this structured approach, we’ve successfully integrated Redux into Next.js 15 while keeping the architecture clean and scalable.
🔹 Key Takeaways:
✅ Use a dedicated MainProvider
to wrap all global providers.
✅ Use a factory function for Redux store to allow future scalability.
✅ Create typed Redux hooks for better TypeScript support.
✅ Use useRef
in ReduxProvider
to prevent unnecessary reinitialization.
Now, Redux is seamlessly integrated into Next.js 15! 🚀
Previous Blogs You Might Find Useful
📌 Next.js 15 API Error Handling
📌 Zod Validation in Next.js 15
Top comments (0)