Introduction
I believe that the reader of this article has a fair knowledge of what Redux and state management are.
I'll explain what Redux is but first, let me explain what state management is.
State explained
When we say state in this context, regarding frontend apps and Redux, we mean the application state. State is the amount of information that the application is comprised of (what the user has inputted or what was pre-configured by the developer). The state controls the behavior of the application. State management is the way we maintain this information in the application.
What is Redux
Redux is a predictable state container for JavaScript apps.
You may ask, "what do they mean by predictable?" In this context predictable means that using Redux, you'll know what every single action in the application will do and how state will change.
In the introduction, we talked about state management. State management is quite a tough and necessary task when developing an application. That's where Redux comes in. Redux is a state management tool that:
helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time-traveling debugger.
Where does "Toolkit come in"
We've heard many frontend developers say "I don't like using Redux". So many times, this dislike comes from the rigorous process of installing and setting up Redux in applications. Redux Toolkit is a package created by the developers of Redux to help simplify the Redux development process. It takes away the stress of configuring the Traditional Redux in applications.
As explained in their docs:
The Redux Toolkit package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
Installing Redux Toolkit
Now, that we have talked about what state is and how redux helps manage state, let's talk about how to install Redux in our Next JS application. In this article, we are going to make use of the example in the official documentation.
We will make use of yarn to install packages. You can use npm for your own project
There are two ways to install the package.
When you are starting a new project, you can use the official Redux+JS template which takes advantage of Redux Toolkit and React Redux's integration with React components:
npx create-react-app my-app --template redux
When you have an already existing project, you can just:
yarn add @reduxjs/toolkit
But to get the application running you need to also install the React Redux package:
yarn add react-redux
You can run all in one line:
yarn add @reduxjs/toolkit react-redux
Setting Up the Store
After installation, the next step is to create a Redux store. In Redux toolkit you do that by creating a file called store.js. You can put it in the root of your file directory or in a folder.
In the store.js file, import the configureStore API from Redux Toolkit.
// store.js
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
Redux Provider
Once the store is created, we can make it available to our Nextjs components by putting a React-Redux around our application in pages/_app.js. Import the Redux store we just created, put a around your , and pass the store as a prop:
// pages/_app.js
import Page from '../components/Page.jsx'
//Import redux provider and store
import {Provider} from 'react-redux'
//store is in the root folder
import store from './../store'
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Page>
<Component {...pageProps} />
</Page>
</Provider>
)
}
export default MyApp
Creating Slice
Create a folder to store your slice. A slice is a:
function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.
The slice is where you perform the state action you want to carry out. In our example, we are increasing and decreasing the number count. We will create a counter slice. This counter slice is where the action is increasing and decreasing function takes place.
So we have a folder /slice. In that folder, we create a file /slice/counterSlice.js. In that file, import the createSlice API from Redux Toolkit.
Creating a slice requires a string name to identify the slice, an initial state value, and one or more reducer functions to define how the state can be updated. Once a slice is created, we can export the generated Redux action creators and the reducer function for the whole slice.
// slice/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
}
//The name of the slice is counterSlice
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
import slice to store
Earlier, we created an empty store. Now, we are going to add the counterSlice to the store. By defining a field inside the reducer parameter, we tell the store to use this slice reducer function to handle all updates to that state.
// store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './slice/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
useSelector and useDispatch
After setting up the store and slice we can now use the React-Redux hooks to let React components interact with the Redux store. React-Redux provides two interactive tools:
- useSelector: We can read data from the store with useSelector
- useDispatch: Used for performing (dispatching) actions.
In your next js app create a component called components/Counter.js. This component is where we will make use of the useSelector and useDispatch hooks.
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment, incrementByAmount } from '../slice/counterSlice'
export function Counter() {
//Destructure count value from counter slice
const { value } = useSelector((state) => state.counter)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2');
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() =>
dispatch(incrementByAmount(Number(incrementAmount) || 0))
}
>
Add Amount
</button>
</div>
</div>
);
}
So there you have it. You now have a Redux store set up using Redux Toolkit.
We should not forget to create a page to display the counter component.
// pages/index.js
import Counter from '../components/Counter'
export default function Home() {
return (
<div>
<Counter />
</div>
)
}
Now, when we open our app, the counter form is displayed on the first page.
NextJs + Redux Gotcha
In order to fully enjoy the server-side rendering capabilities of NextJs, we will need an additional package.
When we need to deal with server-side rendering in our app, things start to get complicated as another store instance is needed on the server to render Redux-connected components.
We may also need to access the Redux Store during a page's getInitialProps.
This is where we add the next package called Next-Redux-Wrapper:
It automatically creates the store instances for you and makes sure they all have the same state.
Install the package using:
yarn add next-redux-wrapper
Then we can modify our store to accommodate next-redux-wrapper
// store.js
import counterReducer from './slice/counterSlice'
import { createWrapper } from 'next-redux-wrapper'
export const store = () =>
configureStore({
reducer: {
counter: counterReducer,
},
});
export const wrapper = createWrapper(store, { debug: true });
Then we modify our _app.js as follows:
// pages/_app.js
import Page from '../components/Page.jsx'
import {wrapper} from '../store.js'
function MyApp({ Component, pageProps }) {
return (
<Page>
<Component {...pageProps} />
</Page>
)
}
export default wrapper.withRedux(MyApp)
With this done, there will be no conflict between Redux and NextJs.
Summary
We've successfully added Redux Toolkit to our NextJs app in this article. We looked at how Redux takes away state management rigors, and how we can also take away more burdens by using Redux Toolkit.
We talked about the two hooks provided by redux to interact with our Redux store called useSelector and useDispatch.
We talked about how there can be a conflict between the NextJs app and Redux in cases of server-side rendering. In this case, we use next-redux-wrapper.
I hope you gained a thing or two from this article. Please share, comment, and leave a reaction.
Have a nice day! Cheers!