In this tutorial, you’ll learn how to set up and use Redux Toolkit and RTK Query in your Next.js 13 project. It’s worth noting that at the time of writing, Next.js 13 is still in beta.

Previously, React could only render on the client side, but with the release of React 18, a new feature called server components was introduced. Next.js 13 has adopted this feature and it’s located within the /app directory.

So, what exactly are server components? Simply put, they allow you to render components on the server, which reduces the amount of JavaScript that needs to be sent to the client. All components within the Next.js 13 /app directory are Server Components by default.

To demonstrate how we can use client-side rendering in Next.js 13, we’ll be creating a simple counter component that uses Redux Toolkit to manage its state. We’ll also use RTK Query to show you how to fetch data within the Next.js 13 app directory.

More practice:

How to Setup Redux Toolkit in Next.js 13 App Directory

Setup the Next.js 13 Project

By following this tutorial, you’ll be able to create an app that looks like the preview screenshot below. The app includes a counter component and a list of users that are fetched from an API and displayed below the counter.

Using Redux Toolkit and RTK Query in Next.js App Directory

Also, the folder and file structure of your project will resemble the one shown in the screenshot below.

Folder Structure of the Next.js 13 and Redux Toolkit Project

To begin, navigate to a location on your computer, such as your Desktop, and open a new terminal in that directory. From there, you can generate a new Next.js 13 project by running one of the following commands, depending on your preferred package manager.


yarn create next-app nextjs13-redux-toolkit
# or 
npx create-next-app@latest nextjs13-redux-toolkit

After running the command, you’ll be prompted to enable some features before the project can be generated. Choose “Yes” for TypeScript and Eslint. You’ll then be asked if you want to use the src directory for the project, so select “Yes“.

Next, you’ll be prompted to enable the experimental app/ directory for the project, which is what we’ll be using in this tutorial, so choose “Yes“. Finally, you’ll be asked if you want to enable import alias, so press TAB to accept and then press Enter.

After answering all the questions, the Next.js 13 project will be generated and all necessary dependencies will be installed. Once the project is generated, you can open it in your preferred IDE. Before diving into how to use Redux Toolkit in the app directory, it’s a good idea to ensure everything is working correctly.

To do this, open the integrated terminal in your IDE and run either yarn dev or npm run dev depending on your package manager. This will start the Next.js dev server.

You can then view the project in your browser by navigating to http://localhost:3000/. However, you may notice an error in the Console tab of your browser dev tool related to hydration. Don’t worry, the Next.js team is likely to provide a solution to this error by the time you read this article.

warning in the terminal about hydration

Setup the Redux Store

With our Next.js 13 project set up, it’s time to move on to setting up the Redux store. Navigate to the “src” directory and create a new folder called “redux“. Inside this folder, create a file named store.ts and add the following code to it.

src/redux/store.ts


import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
  devTools: process.env.NODE_ENV !== "production",
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

In the above code snippet, we utilized the configureStore function from the Redux Toolkit library to create a new instance of the Redux store. This function allows us to specify options and default middleware for the store. Here, we created an empty store with no middleware or reducers.

We also exported the store instance from the file so that other parts of the application could access it.

Finally, we extracted the RootState and Dispatch types from the store instance at the bottom of the file. By inferring these types from the store, we ensure that they stay up to date as we add new state slices or make changes to middleware settings.

Define Typed Hooks

Instead of importing the RootState and AppDispatch types into each component, it’s recommended to create typed versions of the useDispatch and useSelector hooks. This helps avoid potential circular import dependency issues and makes it easier to use these hooks across your application.

To do this, create a hooks.ts file within the “redux” directory and define the following hooks in it.

src/redux/hooks.ts


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

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Define a Custom Provider

In earlier versions of Next.js, we would typically wrap the Redux provider around the main entry point of the application. However, with Next.js 13, every component is now a server-side component. This means we need to create a custom provider component that lives on the client-side and wrap it around the children nodes.

To create this custom provider component, we’ll create a new file named provider.tsx within the “redux” directory. We’ll use the "use client" flag at the top of the file to indicate that the provider component should live on the client-side. Once you’ve created the file, add the following code to it.

src/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>;
}

Provide the Redux Store to Next.js 13

Now that we have a custom provider component that runs on the client-side, we can incorporate it into the layout.tsx file, which acts as the template for our application layout. By wrapping the custom provider component around the children nodes, all of our components will be able to access the Redux store.

To accomplish this, navigate to the src/app/layout.tsx file and replace its existing code with the following block.

src/app/layout.tsx


// import "./globals.css";
import { Providers } from "@/redux/provider";

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

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

Create the Redux State Slice and Action Types

With the Redux store and provider component set up, we can now utilize Redux to manage the state of our application. To start, we will create a “slice” file that will be responsible for managing the state of a counter component.

Each slice file can be thought of as a specific piece of state in our application, and they are typically located within the “features” directory in the “redux” folder.

To create the counter slice, navigate to the “redux” directory and create a new folder called “features“. Inside the “features” folder, create a new file named counterSlice.ts and add the following code to it.

src/redux/features/counterSlice.ts


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

type CounterState = {
  value: number;
};

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

export const counter = createSlice({
  name: "counter",
  initialState,
  reducers: {
    reset: () => initialState,
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
    decrementByAmount: (state, action: PayloadAction<number>) => {
      state.value -= action.payload;
    },
  },
});

export const {
  increment,
  incrementByAmount,
  decrement,
  decrementByAmount,
  reset,
} = counter.actions;
export default counter.reducer;

Add the Slice Reducer to the Store

To enable Redux to manage the counter state, we need to incorporate the reducer function from the counter slice into our store. The reducer function will be responsible for handling all updates to the counter state.

To achieve this, we will import the reducer function from the counter slice into the src/redux/store.ts file and include it in the reducer parameter of the configureStore function. This will allow our Redux store to use the reducer function to manage updates to the counter state.

src/redux/store.ts


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

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

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Use Redux State and Actions in the Next.js 13 App Directory

Let’s create a counter component that will use the counter slice to manage the state of our application. We’ll write this component in the src/app/page.tsx file, but first we need to indicate that this code should live on the client-side using the "use client" flag at the beginning of the file.

To get started, open the src/app/page.tsx file and replace its content with the following code.

src/app/page.tsx


"use client";

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

export default function Home() {
  const count = useAppSelector((state) => state.counterReducer.value);
  const dispatch = useAppDispatch();

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <button onClick={() => dispatch(increment())}>increment</button>
        <button
          onClick={() => dispatch(decrement())}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => dispatch(reset())}>reset</button>
      </div>
    </main>
  );
}

Once you have completed the previous steps, you can now start the Next.js development server and navigate to the root route at http://localhost:3000/ to begin interacting with the counter component.

Here, you’ll be able to increase, decrease, and reset the counter to test that Redux is effectively managing the state of the application.

testing the Redux Toolkit Counter Example in the Next.js 13 App Directory

Create the RTK Query API Service

Let’s see how we can use RTK Query to fetch data from an API and handle server-side state in our Next.js 13 project. Server-side state is usually defined in a “services” directory located within the “redux” folder.

To create the API service, we can simply create a userApi.ts file inside the “services” directory and copy the following code into it.

src/redux/services/userApi.ts


import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

type User = {
  id: number;
  name: string;
  email: number;
};

export const userApi = createApi({
  reducerPath: "userApi",
  refetchOnFocus: true,
  baseQuery: fetchBaseQuery({
    baseUrl: "https://jsonplaceholder.typicode.com/",
  }),
  endpoints: (builder) => ({
    getUsers: builder.query<User[], null>({
      query: () => "users",
    }),
    getUserById: builder.query<User, { id: string }>({
      query: ({ id }) => `users/${id}`,
    }),
  }),
});

export const { useGetUsersQuery, useGetUserByIdQuery } = userApi;

The userApi.ts file above defines two endpoints: getUsers and getUserById. The getUsers endpoint fetches a list of users from https://jsonplaceholder.typicode.com/, while the getUserById endpoint retrieves a user by their Id.

Additionally, we set refetchOnFocus: true to ensure that when the application window is refocused, all subscribed queries are refetched by RTK Query.

Add the RTK Query API Service to the Store

Now that we have defined our API service, we need to integrate it into our Redux store. This involves adding the service’s reducer to the reducer parameter and its middleware to the middleware parameter of the configureStore function.

In addition, we must call setupListeners(store.dispatch) to enable the refetchOnFocus feature.

To do this, open the src/redux/store.ts file and replace its contents with the following code snippets.

src/redux/store.ts


import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";
import { userApi } from "./services/userApi";
import { setupListeners } from "@reduxjs/toolkit/dist/query";

export const store = configureStore({
  reducer: {
    counterReducer,
    [userApi.reducerPath]: userApi.reducer,
  },
  devTools: process.env.NODE_ENV !== "production",
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({}).concat([userApi.middleware]),
});

setupListeners(store.dispatch);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Use RTK Query in the Next.js 13 App Directory

We’ve now created the counter slice and API service, so let’s put them to use within the src/app/page.tsx file. Open this file and replace its contents with the code provided below.

src/app/page.tsx


"use client";

import { useGetUsersQuery } from "@/redux/services/userApi";
import { decrement, increment, reset } from "@/redux/features/counterSlice";
import { useAppDispatch, useAppSelector } from "@/redux/hooks";

export default function Home() {
  const count = useAppSelector((state) => state.counterReducer.value);
  const dispatch = useAppDispatch();

  const { isLoading, isFetching, data, error } = useGetUsersQuery(null);

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <button onClick={() => dispatch(increment())}>increment</button>
        <button
          onClick={() => dispatch(decrement())}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => dispatch(reset())}>reset</button>
      </div>

      {error ? (
        <p>Oh no, there was an error</p>
      ) : isLoading || isFetching ? (
        <p>Loading...</p>
      ) : data ? (
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr 1fr 1fr",
            gap: 20,
          }}
        >
          {data.map((user) => (
            <div
              key={user.id}
              style={{ border: "1px solid #ccc", textAlign: "center" }}
            >
              <img
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                style={{ height: 180, width: 180 }}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      ) : null}
    </main>
  );
}

Congratulations! You’ve successfully integrated both the counter slice and API service in the src/app/page.tsx file. You can now start the Next.js dev server and go to http://localhost:3000/ to see the application in action.

On the page, you’ll find the counter component along with a grid displaying a list of users fetched from the https://jsonplaceholder.typicode.com/ API.

Using Redux Toolkit and RTK Query in Next.js App Directory

Conclusion

In this article, you’ve learned how to set up and use Redux Toolkit and RTK Query in your Next.js 13 app. Additionally, we’ve demonstrated how you can create a basic counter component to showcase the power of the Redux store within your Next.js project. By implementing these tools, you can effectively manage your application state and fetch data from an API in a more organized and efficient manner.

We hope you found this tutorial helpful. If you have any feedback or questions, please let us know in the comments. You can also find the source code for the project on GitHub.