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
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;
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;
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>();
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>;
}
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>
);
}
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>
);
}
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)