There are several ways to manage state in Next.js. In this guide, we will explore how to manage state in Next.js using Redux Toolkit.
State management plays such an important role in today’s web applications that it is impossible to overstate its importance. It is the way an application manages and processes data throughout its development lifecycle.
In your favorite ecommerce app, when you log in, add items to your shopping cart and finally check out, all these steps are handled using state management. There are several ways to manage state in Next.js, but in this guide, we will focus on using Redux Toolkit.
Redux is one of the most popular state management tools in the React ecosystem. One of its strengths is its ability to handle complex state management, especially in large applications where passing data can become complex. It provides a solution by creating a store that centralizes the state and allows one-way data flow.
While Redux is a powerful tool for state management, setting it up and the need to write repetitive boilerplate code can be complex and potentially impact code performance. This is where the Redux Toolkit comes in. Redux Toolkit, also known as RTK, is a set of utilities and functions that simplifies working with Redux.
Let’s set up a Next.js application and install the necessary dependencies. Run the following command in your terminal:
npx create-next-app@latest my-nextjs-app
After that, navigate to the project directory and install these dependencies.
cd my-nextjs-app
npm install @reduxjs/toolkit react-redux firebase
Here, we installed Redux, Redux Toolkit and Firebase. We will build a simple application that tracks whether a user is logged in or not using Redux Toolkit and Firebase as our backend.
To initialize Firebase in your project, create a file named firebase.js
at the root directory of the project. Head over to your Firebase console to create a new project and copy your Firebase configuration code into the firebase.js
file.
// Import the functions needed from the SDKs
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "<<YOUR-API-KEY>>",
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
We also imported the Firebase product we will use; in our case, it is Firebase SDK for authentication.
A store in Redux is the central repository that holds the entire state of your application. It manages the state and enforces a predictable data flow in an application. A Redux store can be considered a single source of truth for the state of an application.
To create a Redux store, create a new folder in your app’s root directory named Redux
. Inside this folder, create a new file store.js
and add the following to it:
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../slices/authSlice";
const store = configureStore({
reducer: {
auth: authReducer,
},
});
export default store;
In the code above, we set up a Redux store with a single reducer to manage the application’s auth state. The configureStore
function is a utility created by the Redux Toolkit to simplify the process of creating and configuring a Redux store.
A Reducer is a function that specifies how the application’s state changes in response to actions. In our case, authReducer
is the reducer responsible for managing the authentication state.
A slice in the Redux Toolkit is a collection of Redux logic for a single feature in your application. A slice possesses three important elements: Reducers, State and Actions.
Reducers are pure functions that handle how the state changes in response to specific actions, while actions are objects that represent user interactions.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
isLoggedIn: false,
displayName: null,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
SET_ACTIVE_USER: (state, action) => {
state.isLoggedIn = true;
state.displayName = action.payload.displayName;
},
REMOVE_ACTIVE_USER: (state, action) => {
state.isLoggedIn = false;
state.displayName = null;
},
},
});
export const { SET_ACTIVE_USER, REMOVE_ACTIVE_USER } = authSlice.actions;
export default authSlice.reducer;
The code above defines the Redux slice for authentication. It uses the createSlice
function to simplify the process of creating Redux slices. The createSlice
function is called with an object that contains the slice’s name (auth
), initial state (initialState
) and a set of reducer functions (SET_ACTIVE_USER
and REMOVE_ACTIVE_USER
).
Reducer functions define how the state should be updated in response to dispatched actions.
In Redux, a Provider component wraps your entire application to make the Redux store accessible to all components. To create a custom provider, in your Redux
folder, create a file named ReduxProvider.js
and add the following to it:
"use client";
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
function ReduxProvider({ children }) {
return <Provider store={store}>{children}</Provider>;
}
export default ReduxProvider;
Here, you will notice that we created a custom provider instead of wrapping the provider directly to our root app, layout.js
. This is because we need this component to be client-side rendered.
Finally, we need to update our root app
with the custom provider. Unlike conventional React applications, Next.js 14 does not have an app.js
or index.js
file, so the entry point in Next.js applications is the layout.js
file.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import ReduxProvider from "../Redux/ReduxProvider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode,
}>) {
return (
<html lang="en">
<body className={inter.className}>
{/* import ReduxProvider */}
<ReduxProvider>{children}</ReduxProvider>
</body>
</html>
);
}
In this file, we imported the custom provider we just created—ReduxProvider
—and wrap it around the children
in the layout.js
file. This makes it accessible to other components of the app.
Now that we have finished setting up Redux Toolkit in our code, we need to test its functionality in our component.
First, head over to your Firebase console and enable Google Provider.
Create a folder named components
in your app
directory. Then create a file in your components
folder named Auth.js
, add the following code to it:
"use client";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { auth } from "../../firebase";
import { signInWithPopup, GoogleAuthProvider, signOut } from "firebase/auth";
import { SET_ACTIVE_USER, REMOVE_ACTIVE_USER } from "@/Redux/authSlice";
const Auth = () => {
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
const displayName = useSelector((state) => state.auth.displayName);
const signInWithGoogle = async () => {
setLoading(true);
const provider = new GoogleAuthProvider();
try {
const result = await signInWithPopup(auth, provider);
const user = result.user;
dispatch(SET_ACTIVE_USER({ displayName: user.displayName }));
} catch (error) {
console.error("Error signing in with Google", error);
} finally {
setLoading(false);
}
};
const signOutUser = async () => {
setLoading(true);
try {
await signOut(auth);
dispatch(REMOVE_ACTIVE_USER());
} catch (error) {
console.error("Error signing out", error);
} finally {
setLoading(false);
}
};
return (
<div>
{loading ? (
<p>Loading...</p>
) : isLoggedIn ? (
<div>
<p>Welcome, {displayName}!</p>
<button className="text-center mt-8" onClick={signOutUser}>
Sign Out
</button>
</div>
) : (
<button onClick={signInWithGoogle}>Sign In with Google</button>
)}
</div>
);
};
export default Auth;
In the code above, we used Firebase Google auth and Redux to track whether or not a user is logged in. When the user is logged in, it displays the user’s name.
We also used the useDispatch
hook to send actions to our Redux store. It is the only way we can update the store within our components. It provides a way to dispatch actions from components without the need to pass the dispatch
function down through multiple props.
The SET_ACTIVE_USER
sets the state to indicate that a user is logged in and updates the display name based on the provided payload, while the REMOVE_ACTIVE_USER
sets the state to indicate that no user is currently logged in, resetting the display name to null.
The useSelector
is a hook that takes a selector
function as an argument. The selector
function is used to extract a specific piece of data from the Redux store. In our case, it extracts isLoggedIn
and displayName
properties from the auth
slice of the Redux store.
Finally, we need to import our Auth.js
file into the page.tsx
file, which is the main component in our application.
import Image from "next/image";
import Auth from "./components/Auth";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<Auth />
</main>
);
}
If you have successfully followed up to this point, you should be able to sign in with Google.
Redux Toolkit is a state management tool for complex applications. However, just like any other tool, it has its drawbacks.
If you are building a small-scale application with a simple state management requirement and does not involve complex state interactions, introducing the Redux Toolkit can be overkill. Next.js provides a built-in state management system that is sufficient for smaller applications.
If the data your components need is easily accessible through Next.js’s data fetching mechanisms in Server Components, managing it globally in Redux might be unnecessary.
In this article, we explored the Redux Toolkit, learned how to use it to manage state in Next.js, and implemented Firebase Google authentication.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.