DEV Community

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

Posted on

1

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

AWS Q Developer image

Your AI Code Assistant

Ask anything about your entire project, code and get answers and even architecture diagrams. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Start free in your IDE

Top comments (0)

๐Ÿ‘‹ Kindness is contagious

If this post resonated with you, feel free to hit โค๏ธ or leave a quick comment to share your thoughts!

Okay