DEV Community

Cover image for Setting up Redux Toolkit in NextJs
Femi Abimbola
Femi Abimbola

Posted on

Setting up Redux Toolkit in NextJs

Managing global state in modern web applications is important for scalability. While Next.js provides powerful server-side capabilities, managing client-side state across complex component trees often requires a robust solution. That is where Redux Toolkit comes in.

Redux Toolkit is the official, opinionated, battery-included toolset for efficient Redux development. That is, it is recommended by the creators of Redux. However, integrating it with the Next.js requires a specific approach because of the distinction between Server and Client Components in Next.js. Since the Redux store relies on React Context, it must be implemented strictly within the client boundary.

In this guide, we will set up a type-safe Redux store, create slices, and integrate them seamlessly into a Next.js application.

Step 1: Install Dependencies

First, we need to install the core libraries. We need the toolkit itself, the React bindings, and the TypeScript types.

Run the following command in your terminal

npm install @reduxjs/toolkit react-redux @types/react-redux
Enter fullscreen mode Exit fullscreen mode

Step 2: Organize Your Directory

It is best practice to keep your state logic organized. Create a folder named redux (or lib/redux) at the root of your project. This is where your store, hooks, and providers will live.

Inside this folder, create a subfolder called features to hold your slices.

Step 3: Create a Slice

A "slice" is a collection of Redux reducer logic and actions for a single feature of your app. Let's create a counter example.

Create redux/features/counterSlice.ts:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../store";

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers.
      // It doesn't actually mutate the state because it uses the Immer library.
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Selector to access the data from the store
export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure the Store

Now, we need to create the store that brings all slices together.

Create redux/store.ts:

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode

Step 5: Create Typed Hooks

While you can use the standard useDispatch and useSelector from react-redux, it is better to create typed versions. This saves you from having to define (state: RootState) every time you select data and ensures your dispatch handles thunks correctly.

Create redux/hooks.ts:

import { useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
Enter fullscreen mode Exit fullscreen mode

Step 6: Create the Provider

This is the most critical step for Next.js App Router applications. Because Provider uses React Context, it must be a Client Component. We cannot render it directly in a Server Component (like layout.tsx) without isolating it first.

Create redux/provider.tsx:

"use client";

import { store } from "./store";
import { Provider } from "react-redux";

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Integrate with the Root Layout

Now that we have a client-side Provider, we can wrap our application's children with it in the root layout.

Open app/layout.tsx:

import { Providers } from "@/redux/provider";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Using Redux in Components

You are now ready to use the state in your application. Remember, any component using hooks must be a Client Component ("use client").

Here is the example Counter component:

"use client";

import { useAppSelector, useAppDispatch } from "../redux/hooks";
import { decrement, increment } from "../redux/features/counterSlice";

export function Counter() {
  // Use the typed hooks we created earlier
  const dispatch = useAppDispatch();
  const count = useAppSelector((state) => state.counter.value);

  return (
    <div style={{ padding: "20px", textAlign: "center" }}>
      <button
        aria-label="Increment value"
        onClick={() => dispatch(increment())}
        style={{ marginRight: "10px" }}
      >
        Increment
      </button>

      <span style={{ fontSize: "20px", fontWeight: "bold" }}>
        {count}
      </span>

      <button
        aria-label="Decrement value"
        onClick={() => dispatch(decrement())}
        style={{ marginLeft: "10px" }}
      >
        Decrement
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Setting up Redux Toolkit with Next.js requires careful separation of Server and Client logic. By creating a dedicated Providers component, you can enjoy the robust state management of Redux without sacrificing the performance benefits of Next.js Server Components.

Top comments (0)