Editor’s note: This post was updated on 22 February 2023 to include information about persisting the authentication state in Next.js and to provide additional insight into when to use or avoid using Redux with Next.js.
Redux is one of the most popular state management solutions in the React ecosystem. Nowadays, there are plenty of alternatives, but Redux is still the most trusted and widely used tool.
For this reason, many projects that use Next.js want to take advantage of Redux as well. But using Redux in a Next application has a few catches, and the setup is not always straightforward. That’s why this article will walk you through how we can set up a Next.js project with Redux.
Contents
- Why should you use Redux with Next.js?
- Building a sample app with Next.js and Redux
- Using the Redux store
- Persisting state in Next.js
- Persisting the authentication state in Next.js
- When should you avoid using Redux with Next.js?
Why should you use Redux with Next.js?
There are a lot of reasons why you might want to use Redux in a Next application. Let’s take a look at some of them.
Sharing state
Usually, a central state is used to manage the shared data between the components in a tree. In React, data flows only downwards, which means you can pass data from the parent component to a child component.
This limitation sometimes makes things hard because the components might not be close in the component tree, and there might not even be a parent-child path.
In this case, using a common store that wraps all the components makes total sense, and you might consider Redux.
Redux is very powerful
Redux is very powerful as a state management solution. It’s been around for a while, so it has excellent community support.
If you are building something severe and unsure which use cases might appear in the future, more likely than not, Redux will have a solution for you. While nothing is entirely future-proof, Redux is a safe bet for long-term projects.
Everybody knows Redux
In many projects, speed is often a priority. Many React developers are already familiar with Redux, and companies often want to use the same tool, if possible, across all of the projects.
This means even if you are working in a company building a new project in Next, you might be forced to use Redux anyway, so it’s a good idea to learn how to use it based on popularity alone.
Redux is very flexible
One of the major reasons why Redux is so popular is its flexibility. Redux provides a wide range of features, middleware, caching, and performance. It has a very active and vibrant developer community. Also, Redux can separate the concerns inside your application, resulting in better code management in the future.
If you start using Redux in your application, you won’t be limited by its features. In fact, it will open up a lot of new possibilities. You can choose to incorporate these features in your application or not.
Even if you choose to not use Redux, learning a new concept is never bad! So, let’s dive in and see how we can integrate Redux into a Next.js application.
Building a sample app with Next.js and Redux
Today we will build a simple application that tracks if a user is logged in or not, then, based on the state, changes the text above the button:
This project aims to demonstrate how to use Redux, so I am keeping things simple here so we can focus on the Redux integration with Next. Going forward, we have two options. We can use plain Redux, or we can use Redux Toolkit.
Redux is being used in many legacy projects, but Redux Toolkit is recommended, as it reduces a lot of boilerplate code and has improved performance. However, the setups are almost the same for both of these.
Let’s create the starter project by running the following command:
yarn create next-app --typescript
You can see the project in action by running yarn dev
and visiting http://localhost:3000/ on your browser.
Installing the dependencies
Let’s install the required dependencies for Redux Toolkit:
yarn add @reduxjs/toolkit react-redux
As we are using Next, we will need an additional package to take care of our server-side rendering:
yarn add next-redux-wrapper
Creating the slice
Let’s create a new folder called store
and create a file named authSlice.ts
inside it. The official documentation defines a slice as: “a collection of Redux reducer logic and actions for a single feature in your app.”
We will put the logic for our authState
inside of this authSlice.ts
file:
import { createSlice } from "@reduxjs/toolkit"; import { AppState } from "./store"; import { HYDRATE } from "next-redux-wrapper"; // Type for our state export interface AuthState { authState: boolean; } // Initial state const initialState: AuthState = { authState: false, }; // Actual Slice export const authSlice = createSlice({ name: "auth", initialState, reducers: { // Action to set the authentication status setAuthState(state, action) { state.authState = action.payload; }, }, // Special reducer for hydrating the state. Special case for next-redux-wrapper extraReducers: { [HYDRATE]: (state, action) => { return { ...state, ...action.payload.auth, }; }, }, }); export const { setAuthState } = authSlice.actions; export const selectAuthState = (state: AppState) => state.auth.authState; export default authSlice.reducer;
This is a straightforward slice. A slice for any normal React application using Redux will be just like this. There is nothing special for Next yet.
The only thing we are doing here is defining the authState
in our store and creating the action for setting the authState
named setAuthState
.
In line 27, you will notice a special reducer we are adding here called HYDRATE
. The HYDRATE
action handler must properly reconcile the hydrated state on top of the existing state (if any).
Basically, when any page refresh occurs, if you navigate from one page to another page, or the getStaticProps
or the getServerSideProps
functions are called, a HYDRATE
action will be dispatched at that moment. The payload
of this action will contain the state at the moment of static generation or server-side rendering, so your reducer must merge it with the existing client state properly.
Creating the store
Next, create a file named store.ts
to create the store, and add our authSlice
there:
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; import { authSlice } from "./authSlice"; import { createWrapper } from "next-redux-wrapper"; const makeStore = () => configureStore({ reducer: { [authSlice.name]: authSlice.reducer, }, devTools: true, }); export type AppStore = ReturnType<typeof makeStore>; export type AppState = ReturnType<AppStore["getState"]>; export type AppThunk<ReturnType = void> = ThunkAction< ReturnType, AppState, unknown, Action >; export const wrapper = createWrapper<AppStore>(makeStore);
Notice on line 22 where we export a special wrapper
function. This wrapper eliminates the need for a Provider
that we would use in a normal React application.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Updating the app
We have to do one last thing to finish setting up our Redux architecture. Open the _app.tsx
file and wrap our component like so:
import "../styles/globals.css"; import type { AppProps } from "next/app"; import { wrapper } from "../store/store"; function MyApp({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; } export default wrapper.withRedux(MyApp);
Notice at line 9 that we are wrapping our component with withRedux
. We can also wrap the individual pages, but that is not recommended; when we navigate from one page to another, if that particular page is not wrapped, it will crash.
Using the Redux store
Our Redux setup is complete! Let’s use our Redux store inside the index.tsx
page like so:
import type { NextPage } from "next"; import { selectAuthState, setAuthState } from "../store/authSlice"; import { useDispatch, useSelector } from "react-redux"; const Home: NextPage = () => { const authState = useSelector(selectAuthState); const dispatch = useDispatch(); return ( <div> <div>{authState ? "Logged in" : "Not Logged In"}</div> <button onClick={() => authState ? dispatch(setAuthState(false)) : dispatch(setAuthState(true)) } > {authState ? "Logout" : "LogIn"} </button> </div> ); }; export default Home;
Any Redux store has two main purposes: reading and updating.
On line 6, you can see we are reading the state using the useSelector
function provided by react-redux
.
We have a button where we can toggle the authState
, and based on this, we are changing the text on the button.
Persisting state between page navigation in Next.js
Now we have successfully set up our Redux store. You can verify it by clicking the button, which will dispatch actions based on the current state and update the store, which will eventually change the state.
But if you refresh your page, you will see that the state doesn’t persist. This is because, in Next, each page is rendered on demand, which means when you navigate from one page to another, the previous state will be gone.
For this case, if the user is logged in, then whenever you switch to another page, the user will be logged out automatically as the initial authState
is defined as false.
To resolve this issue, we will take advantage of the wrapper function we created earlier and use Next’s special function getServerSideProps
, as this will get called each time the page loads.
Let’s add the following code to our index.tsx
file:
export const getServerSideProps = wrapper.getServerSideProps( (store) => async ({ params }) => { // we can set the initial state from here // we are setting to false but you can run your custom logic here await store.dispatch(setAuthState(false)); console.log("State on server", store.getState()); return { props: { authState: false, }, }; } );
We are generating the initial state inside the getServerSideProps
function here, so even if you refresh the page; you will see that the state values remain the same.
Persisting the authentication state in Next.js
When it comes to authentication, the state needs to be persisted not only between page transitions but also during a refresh. This is necessary so that users won’t have to log in every time they refresh the page.
To persist the authentication state properly, we can use [redux-persist](https://www.npmjs.com/package/redux-persist)
library. Let’s see how we can integrate this.
First, install the dependency, like so:
yarn add redux-persist
Then, open up store.ts
file and create a new function called makeStore
:
import { persistReducer, persistStore } from "redux-persist"; import storage from "redux-persist/lib/storage"; const rootReducer = combineReducers({ [authSlice.name]: authSlice.reducer, }); const makeConfiguredStore = () => configureStore({ reducer: rootReducer, devTools: true, }); export const makeStore = () => { const isServer = typeof window === "undefined"; if (isServer) { return makeConfiguredStore(); } else { // we need it only on client side const persistConfig = { key: "nextjs", whitelist: ["auth"], // make sure it does not clash with server keys storage, }; const persistedReducer = persistReducer(persistConfig, rootReducer); let store: any = configureStore({ reducer: persistedReducer, devTools: process.env.NODE_ENV !== "production", }); store.__persistor = persistStore(store); // Nasty hack return store; } }; // Previous codes export const wrapper = createWrapper<AppStore>(makeStore);
In the above code, we are trying to understand whether we are dealing with the server or client state. Because on the server side, we don’t need persistence.
Next, we call the makeConfiguredStore
with different configurations based on whether it’s a client or server. When dealing with the client state, we need to create a persistedReducer
by using the persistReducer
function exported by redux-toolkit
. We use the persistConfig
to specify the key and the type of storage. I won’t go into details here as it’s specific to redux-tookit
. Then, we assign this persistStore
to the store.__persistor
and return it.
We need to modify our _app.tsx
file to take advantage of this persisted reducer.
The final code looks like this:
import "../styles/globals.css"; import type { AppProps } from "next/app"; import { wrapper } from "../store/store"; import { PersistGate } from "redux-persist/integration/react"; import { useStore } from "react-redux"; function MyApp({ Component, pageProps }: AppProps) { const store: any = useStore(); return ( <PersistGate persistor={store.__persistor} loading={<div>Loading</div>}> <Component {...pageProps} /> </PersistGate> ); } export default wrapper.withRedux(MyApp);
Basically, we wrap our component with the PersistGate
and pass the store.__persistor
and an optional loading
component to show users when our state is rehydrating.
Now if everything goes well we can now refresh our application, and the authentication state will persist!
Hooray! We successfully added redux-persist
to our application, allowing us to manage authentication.
When should you avoid using Redux with Next.js?
There are some situations where Redux might not be the best option. Like everything else, Redux has some drawbacks, and this is especially true when integrating with Next.js.
Let’s discuss some scenarios when you shouldn’t use Redux.
You are building a small-scale project
If you are only building a small-scale project, then Redux might be overkill. In Next.js, the default state management solutions can and will be able to handle almost all use cases. So unless you build something serious or a proof of concept, you should consider the default state management solutions.
The setup is complex
Integrating Redux in Next.js is a complex procedure. Using Redux may not be your best option if you need to ship a feature fast.
React’s Context API is sufficient
Context is React’s own solution for sharing state between components. Accord to the documentation:
“Context provides a way to pass data through the component tree without having to pass props down manually at every level.“
This is exactly the same thing that Redux is doing. So if you don’t need the fancy features of Redux, then don’t bother using it. For most use cases, React’s Context API is enough.
Conclusion
That’s how you can integrate Redux with a Next.js application! You can find the GitHub repository for this project here. I would also encourage you to review the documentation of next-redux-wrapper to learn more about other use cases.
Have a great day!
I really like this tutorial and I am very grateful for it. But I have one question. I have a lot of pages, how can I maintain a state on every page? In your example you are using SSR, but do I need to invoke it on every page? It would be perfect to use it on layout component or something. Do you have any idea how to do it?
we use redux or useContext for it if it’s an easy thing like logging out you can use two of em.
What am I missing? I followed your article carefully and the state does not persist in the browser. I’ using the latest version of Chrome. I even downloaded your repo, added the node_modules, but even your own repo does not retain the state. I checked my code against your repo code also. What am I missing?
It took me a while to figure out, but it looks like extraReducers should be moved outside of the reducers object in the authSlice.ts file
Rightly said Darin.
Thank you for this great tutorial. That helped already. But one question please. Do we need the extraReducer with the Hydrate type in every single Slice?
Thank you, nice tutorial! Perfect for next/redux beginner.
Hi thanks, could you please update it to useWrappedStore?
I would also appreciate this update.
thank you .