E-CommerceCompetitive PricingMarketing Ready
Dotcreo Digital logo

How To Setup Redux Toolkit In Next.js

Next.js App Router
15 mins read
19th Feb 2419th Feb 24

Introduction

Long gone are the days of just opting for regular Redux. Well, unless you want to write more boilerplate code and overcomplicate things. Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. In this guide, we will be setting up Redux Toolkit with Next.js.

How To Read This Guide

This article assumes you already have an existing Next.js application based on the App Router architecture and a basic understanding of how regular Redux works.

If you want to follow along, you can just create a basic application with npx create-next-app@latest. Remember to choose the App Router option, none of the other default options are necessary for this to work.

Getting Started

Here are the steps we will follow:

  1. Install Redux Toolkit and React-Redux.
  2. Create a Redux store.
  3. Create a Redux slice.
  4. Import our reducers into our store.
  5. Create a Global State Provider file.
  6. Provide the Redux store to your Next.js app.
  7. Dispatch actions and connecting components.

Bonus Tip: Populate state from a RootLayout file

Step One: Installing Redux Toolkit and React-Redux

First, we need to install Redux Toolkit and React-Redux. Run the following command in your terminal: npm install @reduxjs/toolkit react-redux.

This command installs the necessary packages for Redux state management and the integration with React.

Step Two: Creating a Redux Store

Now, let's set up the Redux store for your Next.js app. Create a folder called redux in your project's root directory, and inside that, create a file called store.js.

Open it and add the following code:

import { combineReducers, configureStore } from '@reduxjs/toolkit'

const rootReducer = combineReducers({
  // Reducers will go here afterwards
})

export const makeStore = () => {
  return configureStore({
    reducer: rootReducer
  })
}

Step Three: Creating a Slice of the Redux State

In Redux Toolkit, a "slice" is a reducer and its actions. We'll use an example here of updating a counter. Firstly, let's create a folder called slices in our redux folder created in the previous step. Inside this folder, create a file called counterSlice.js and add the following code:

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    }
  }
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer

🚨 Warning

There's something really important that you need to understand when it comes to using Redux Toolkit rather than plain Redux.

When you look at the increment and decrement reducers, you might be surprised to see that we're directly modifying the state. In traditional Redux, this would be a big no-no. We're always supposed to return a new state object, not modify the existing one.

However, Redux Toolkit uses a library called Immer under the hood. Immer allows us to write code that "mutates" the state, but it's actually operating on a temporary draft state under the hood. When our reducer function finishes running, Immer produces a new, immutable result state based on the changes we made to the draft.

This means we can write simpler, more readable reducer logic that looks like it's directly modifying the state, but still adheres to the principles of Redux. It's one of the key benefits of using Redux Toolkit and can make your Redux code much cleaner and easier to understand.

For further clarification, the following code would not follow the principles of Immer and thus may not work properly in Redux Toolkit:

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state, action) => {
      // Incorrect usage of Immer
      return { value: state.value + 1 }
    }
  }
})

Step Four: Import our reducers into our store

For this step, we need to make some adjustments to our store.js file that we created in the first step. Here's what your new file should look like:

import { combineReducers, configureStore } from '@reduxjs/toolkit'

import counterReducer from '@/redux/counterSlice'

export const rootReducer = combineReducers{
  counter: counterReducer
}

export const makeStore = () => {
  return configureStore({
    reducer: rootReducer
  })
}

Step Five: Create a Global State Provider file

For Next.js App Router architecture, there is another slight difference we need to take into consideration.

The Next.js App Router now supports React Server Components (RSCs), which are exclusively server-rendered. These components can fetch data asynchronously during the render process, eliminating the need for getServerSideProps that was required in the previous Pages Router architecture.

Here are some official Redux usage guidelines for the App Router:

  1. Isolate stores: Instead of using a single global store, instantiate a new Redux store for each request to avoid data leakage.
  2. Avoid using RSCs with Redux: Since RSCs can't utilize hooks or context, they should not interact with a global store.
  3. Minimize store data: Use Redux sparingly and only for global, mutable data.

The instructions provided here are intended for applications using the Next.js App Router architecture. Components that only run on the client-side can continue to use global stores.

Taking the above information into consideration, create a StoreProvider.jsx file marked with the 'use client' directive in the root of your project directory and include the following code:

'use client'

import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '@/lib/store'

export default function StoreProvider({ children }) {
  const storeRef = useRef()
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()
  }

  return <Provider store={storeRef.current}>{children}</Provider>
}

Step Six: Provide the Redux store to your Next.js app

Open the layout.jsx file in the root of your directory and import your StoreProvider file which should wrap the RootLayout component's children.

import { Inter } from 'next/font/google'
import './globals.css'

import StoreProvider from '@/app/StoreProvider'

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

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app'
}

export default function RootLayout({ children }) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <StoreProvider>
          {children}
        </StoreProvider>
      </body>
    </html>
  )
}

This ensures that your entire Next.js app has access to the Redux store.

Step Seven: Dispatching Actions and Connecting Components

Now that you have set up the basics, you can start using Redux Toolkit in your components.

Open any component where you want to use Redux.

Import the necessary functions from React-Redux:

import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from '@redux/slices/counterSlice'

Use useDispatch to dispatch actions:

const dispatch = useDispatch()

Use useSelector to access the state:

const counter = useSelector((state) => state.counter)

Dispatch actions in response to events:

<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>

Connect your component to the Redux store, and you're all set!

You've now successfully integrated Redux Toolkit into your Next.js application. You can expand and customize your Redux store by creating new slices and adding them to your root reducer in store.js.

Bonus Tip: Initializing State with Layout.jsx

Sometimes, you might need to fill your state immediately, irrespective of the page being loaded. In such scenarios, the layout.jsx file proves to be quite useful.

You might be wondering, "Didn't we establish that a Redux store shouldn't interact with a React Server Component (RSC) since they're not client components?" Well, there's a twist to this rule.

Although the general rule is to avoid using Redux with React Server Components (RSCs), the app router architecture introduces an interesting exception. This architecture operates on a dual-rendering mechanism: initially on the server, and subsequently on the client.

This double-rendering mechanism opens up a special opportunity. During the server-side rendering phase, we can prepare and load data into our Redux store via props. However, it's important to note that we're not directly interacting with the store in the server-rendered component. Instead, we're passing the data as props to a client component that can interact with the store. Then, during the client-side rendering phase, our components have access to this pre-loaded state. This approach allows us to effectively manage global state, even within a server-rendered environment, without violating the principle of not using Redux directly in RSCs.

In order for this to work, theres two things we need to do:

  1. Populate the state in the Layout.js file.
  2. Dispatch the action in the StoreProvider.jsx

Step One: Populate the State

In this example, we're interacting with a REST API to fetch user data when someone logs into a dashboard. We're still carrying on with the same layout.jsx file from the previous steps.

import { Inter } from 'next/font/google'
import './globals.css'

import StoreProvider from '@/app/StoreProvider'
import { serverSideApi } from '@/app/serverSideApi'

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

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app'
}

export default function RootLayout({ children }) {
  const user = await serverSideApi
  .get("users/me").then((res) => res.json())

  return (
    <html lang='en'>
      <body className={inter.className}>
        <StoreProvider user={user}>
          {children}
        </StoreProvider>
      </body>
    </html>
  )
}

In this code snippet, I'm importing a module named serverSideApi.js. This module enables me to make API calls within a React Server Component (RSC). I use it to fetch some data that gets stored in a variable named user. This user data is then passed as a prop to the StoreProvider.

Step Two: Modify the StoreProvider.jsx File

The final step reveals why we can perform this operation within a React Server Component (RSC). If you recall, our StoreProvider.jsx is actually a client component that runs on every page. Moreover, it can access the props passed to it from the layout.jsx file, as we did in the previous step. This allows us to utilize the data here.

Again, we're moving on from the same StoreProvider.jsx file in the previous steps, but this time let's assume we've already created a userSlice.js:

'use client'

import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '@/lib/store'

import { setUser } from '@/redux/slices/userSlice'

export default function StoreProvider({ children, user }) {
  const storeRef = useRef()
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()

    // Dispatch the user action
    storeRef.current.dispatch(setUser(user))
  }

  return <Provider store={storeRef.current}>{children}</Provider>
}

In this step, we've added user to the list of props {{ children, user }}. We then dispatch an action using storeRef.current.dispatch(setUser(user)). This action updates our Redux store with the user data fetched during the server-side rendering phase.

By doing this, we've effectively bridged the gap between server-side and client-side rendering in our application. We've leveraged the strengths of both to create a seamless user experience, demonstrating the power and flexibility of the app router architecture in Next.js.

Samuel King headshot
Author: Samuel King
Full Stack DeveloperDesignerMarketer
Book A Free Call →