Next.js 13 app router feature plus server and client side components to some extent brought changes on how we approach integration and state management using redux toolkit. This tutorial is meant to take you through how to add and use redux toolkit in your next.js 13 projects. We will go through a real world example whereby, we need to store authentication data in redux state and in this case it will be persisted. We will also access this data in the some of the components that need it. Assumptions You are comfortable writing react functional components using Typescript. You are familiar with redux toolkit - creating slices and reducer functions You are familiar with Next.js 13 app router You are familiar with the node.js package managers, I am using yarn in this case. If you are a complete beginner in next.js or redux toolkit, this tutorial may not be suitable for you, but i have provided all the code to follow along. Requirements A backend API with login and logout endpoint. To simplify things, i have a simple nest.js backend with a simulation of login and logout functionality in repo. Clone it, run to install packages , and then run . Your are good to go. this yarn yarn run start:dev You also need a next.js13 app, created using the . Again all the code for this tutorial can be found in repo to follow along. npx create-next-app@latest this Let’s get started Here is the file structure of the project. Using the app router feature of next.js 13. Create a Redux Store We need some packages. These are @reduxjs/toolkit and react-redux. However, i added redux-persist and redux-logger. Only redux-logger is optional, the other three are required for the rest of the tutorial. yarn add @reduxjs/toolkit react-redux redux-persist redux-logger After installing the packages let us dive into creating the store. app/store/index.ts import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { useDispatch, TypedUseSelectorHook, useSelector } from "react-redux"; import { persistReducer } from "redux-persist"; // import storage from "redux-persist/lib/storage"; import { authReducer } from "./slices/authSlice"; import storage from "./customStorage"; import logger from "redux-logger"; const authPersistConfig = { key: "auth", storage: storage, whitelist: ["isAuth", "jid"], }; const rootReducer = combineReducers({ auth: persistReducer(authPersistConfig, authReducer), }); export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }).concat(logger), }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; The code above is responsible for creating the redux store for us. Notice the authPersistConfig definition. This code directs the redux store to persist auth data but only the isAuth and jid keys specified in the whitelist array. Also note the storage am using, its not directly from redux-persist. I have a custom implementation that will take care of execution of the code in client and server side. Using the storage directly from redux-persist results to an error in the server side. Below is the customStorage implementation. Inspiration from github thread. this app/store/customStorage.ts "use client"; import createWebStorage from "redux-persist/lib/storage/createWebStorage"; const createNoopStorage = () => { return { getItem(_key: any) { return Promise.resolve(null); }, setItem(_key: any, value: any) { return Promise.resolve(value); }, removeItem(_key: any) { return Promise.resolve(); }, }; }; const storage = typeof window !== "undefined" ? createWebStorage("local") : createNoopStorage(); export default storage; Notice that in the storage definition am creating a dummy store since its impossible to create local storage in the server side. By checking existence of the window object i can choose when to create the storage. Next, we create an authSlice. app/store/slices/authSlice.ts import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; export interface IAuthState { isAuth: boolean; jid: string; } const initialState: IAuthState = { isAuth: false, jid: "", }; export const authSlice = createSlice({ name: "auth", initialState, reducers: { setAuth: (state, action: PayloadAction<boolean>) => { state.isAuth = action.payload; }, setJid: (state, action: PayloadAction<string>) => { state.jid = action.payload; }, }, }); // Action creators are generated for each case reducer function export const { setAuth, setJid } = authSlice.actions; export const authReducer = authSlice.reducer; Here we get the actions and reducer to play around with the auth state. Notice the two keys and . These are the ones we persisted in the store. isAuth jid Last, we need a way to consume the state in our components. Typically, in previous versions of nextjs we could add the provider in the _app.(jsx/tsx) file. In next.js 13 however, with server components being the default for all pages, it can be challenging to have the provider in the root of the app as it is meant to work on the client. This means we have to initialize the provider separately and wrap it only on client side components that need access to the store. app/store/ReduxProvider.tsx import { Provider } from "react-redux"; import { store } from "."; import { persistStore } from "redux-persist"; persistStore(store); // persist the store export default function ReduxProvider({ children, }: { children: React.ReactNode; }) { return <Provider store={store}>{children}</Provider>; } The code above creates the provider for us. Notice the persistStore line of code. Since we need to persist a section of the state, we need this line before supplying the store to the Provider. With the provider in place we can move it to client components that need access to the the store. Create the Next.js 13 Pages We will look into three pages which are essential to test the functionality we want. ie Home,Login and Account pages. On running the client app,and visiting you will see this simple home page. Nothing fancy, just a navbar and text http://localhost:3000 Here is the home page code. app/(navbar)/page.tsx export default function Home() { return ( <div className="p-5"> <h3>Home</h3> </div> ); } Here is the login page code. app/(navbar)/login/page.tsx import ClientLoginFormWrapper from "./ClientFormWrapper"; export default function Login() { return ( <div className="h-screen w-full flex"> <div className="m-auto"> <h1 className="font-bold text-3xl">Login</h1> <br /> <ClientLoginFormWrapper /> </div> </div> ); } code. This is a client side component marked with the directive “use client“ at the top of the file. It has functionality that only works in the client side. Notice the <ReduxProvider/> wrapper. It will work in this case since it is in a client component. This allows us to have the redux store in the components it wraps. app/(navbar)/login/ClientFormWrapper.tsx "use client"; import ReduxProvider from "@/app/store/ReduxProvider"; import LoginForm from "./Form"; export default function ClientLoginFormWrapper() { return ( <ReduxProvider> <LoginForm /> </ReduxProvider> ); } this file has the login form code. Two html input fields and a submit button with both the onchange and onsubmit handlers. app/(navbar)/login/LoginForm.tsx "use client"; import { useAppDispatch } from "@/app/store"; import { setAuth, setJid } from "@/app/store/slices/authSlice"; import { useRouter } from "next/navigation"; import { useState } from "react"; // client side to add interactivity using local state export default function LoginForm() { const [loading, setLoading] = useState(false); const [values, setValues] = useState({ username: "", password: "" }); const router = useRouter(); const dispatch = useAppDispatch(); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setValues({ ...values, [name]: value }); }; const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setLoading(true); try { const res = await fetch("http://localhost:3002/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values), credentials: "include", mode: "cors", }); const data = await res.json(); if (!res.ok) throw new Error(data.message); dispatch(setJid(data.access_token)); dispatch(setAuth(true)); router.push("/account"); } catch (err) { console.error(err); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit}> <div> <div> <label htmlFor="username">Username</label> </div> <input name="username" id="username" type="text" className="w-full border-2 border-gray-900" onChange={handleChange} value={values.username} /> </div> <br /> <div> <div> <label htmlFor="password">Password</label> </div> <input name="password" type="password" className="w-full border-2 border-gray-900" onChange={handleChange} value={values.password} /> </div> <br /> <div> <button type="submit" className="bg-blue-700 p-2 text-white font-bold"> Login </button> </div> </form> ); } Lets take a look at the onSubmit handler. The login endpoint returns a response object with an key. In this component note that i can access the redux store method since it is wrapped by the . I can therefore dispatch the token to the store where it is then persisted. Am also dispatching true value to update the state. After successful login, using the i redirect user to the page. access_token dispatch ReduxProvider isAuth router account With the nest.js backend above running, you can type any dummy username and password and this will take you to the account page. Here is the code. app/(navbar)/account/page.tsx import ClientStuff from "./ClientStuff"; export default function Account() { return ( <div className="p-5"> <ClientStuff /> </div> ); } this is a client component that fetches user data from the backed and shows a welcome message. Notice we can use hooks in this component as it is client component. app/(navbar)/account/ClientStuff.tsx "use client"; import { store, useAppSelector } from "@/app/store"; import { useEffect, useState } from "react"; export default function ClientStuff() { const [loading, setLoading] = useState(true); const [username, setUsername] = useState(""); const jid = store.getState().auth.jid; useEffect(() => { const getData = async () => { try { const res = await fetch("http://localhost:3002/user/profile", { headers: { Authorization: `Bearer ${jid}` }, mode: "cors", }); const data = await res.json(); setUsername(data.sub); setLoading(false); } catch (error) { console.log(error); } }; getData(); }, [jid]); if (loading) return <div>Loading...</div>; return ( <div> <h3 className="font-bold text-4xl">Welcome {username}!</h3> </div> ); } If the app router is still new to you, you might be a little confused by the file structure am using. Let’s discuss a few things about the app router feature. By default, all next.js 13 projects will have a pages folder with a file. This is how it looks like. It is a function component with , and tags. This is the root layout to be used with all the pages in your applications. layout.tsx children prop html body import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={inter.className}>{children}</body> </html> ) } Based on your design and requirements, you will make decisions on how you will layout your application. In this application, i needed a navbar that will be used in all pages. I therefore had to use a concept in next.js 13 called . For more information check out the . This enabled me to group all the pages that needed the navbar inside the folder without affecting the url. route groups docs (navbar) Here is the code in the layout in (navbar) folder. I called it NavLayout app/(navbar)/layout.tsx import MainNavbarClientWrapper from "./NavbarClientWrapper"; export default function NavLayout({ children }: { children: React.ReactNode }) { return ( <main> <div className="w-full h-16 bg-black"> <MainNavbarClientWrapper /> </div> {children} </main> ); } With this layout all the pages in the folder will have the navbar. (navbar) app/(navbar)/NavbarClientWrapper.tsx "use client"; import ReduxProvider from "../store/ReduxProvider"; import MainNavbar from "./Navbar"; export default function MainNavbarClientWrapper() { return ( <ReduxProvider> <MainNavbar /> </ReduxProvider> ); } This is a client component, and hence am able to add the to wrap the component. ReduxProvider MainNavbar app/(navbar)/Navbar.tsx "use client"; import Link from "next/link"; import { MouseEvent, useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../store"; import { setAuth, setJid } from "../store/slices/authSlice"; import { usePathname, useRouter } from "next/navigation"; export default function MainNavbar() { const isAuth = useAppSelector((state) => state.auth.isAuth); const [isAuthenticated, setIsAuthenticated] = useState<boolean>(); const dispatch = useAppDispatch(); const pathname = usePathname(); const router = useRouter(); useEffect(() => { if (isAuth) { setIsAuthenticated(true); } else { setIsAuthenticated(false); } }, [isAuth]); const handleLogout = async (e: MouseEvent<HTMLButtonElement>) => { e.preventDefault(); await fetch("http://localhost:3002/auth/logout", { credentials: "include", mode: "cors", }); const inAuthPage = pathname.startsWith("/account"); if (inAuthPage) { router.push("/login"); } dispatch(setJid("")); dispatch(setAuth(false)); }; return ( <nav className="flex content-between justify-between w-full h-full px-5 text-white"> <div className="w-2/3 h-full flex"> <Link href={"/"} className="my-auto font-bold text-4xl"> Test App </Link> </div> <div className="flex justify-end w-1/3"> {isAuthenticated && ( <> <Link className="m-auto" href={"/account"}> Account </Link> <button onClick={handleLogout}>Logout</button> </> )} {isAuthenticated === false && ( <div className="flex"> <Link href={"/login"} className="m-auto"> Login </Link> </div> )} </div> </nav> ); } In here, we have the logout handler which makes a request to server and blacklists the token. Then we can update the state by dispatching the and setters. With this functionality the user is logged out from the application. jid isAuth Final thoughts Redux remains to be a go to state manager in react apps development. Only by consuming it in client components that we can safely use it without interfering with the next.js server side functionality.