DEV Community

Cover image for Setting Up Redux in Next.js 15 with Typescript
Saiful Islam
Saiful Islam

Posted on

Setting Up Redux in Next.js 15 with Typescript

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

💡 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;
Enter fullscreen mode Exit fullscreen mode

🚀 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;
Enter fullscreen mode Exit fullscreen mode

✅ 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"];
Enter fullscreen mode Exit fullscreen mode

🔥 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>();
Enter fullscreen mode Exit fullscreen mode

🛠 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;
Enter fullscreen mode Exit fullscreen mode

🛒 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)