Next.js – 3 ways for managing entire application state
The purpose of this document is to describe three different ways to manage state in a Next.js application.
Overview
Many modern-day web applications are broken up into a front-end project, which is usually a Single Page Application, and a back-end project, or a REST API. These types of projects are typically managed within separate git repositories. Confusion and frustration can ensue when the REST project needs to change the shape of the data it returns. Why? Because you are likely trying to orchestrate the release of both projects at nearly the same time to ensure little to no downtime, and it only sometimes works out as expected. To further complicate things, sometimes the languages used to develop the back end are different from the front end, and therefore, you may not be able to use the same developers on the front end as you would for the back end and vice versa.
Next.js is a great framework for developing web applications by allowing both the front end and back-end code to be in one project and repository. If you manage a software development team or are part of a software development team this framework could greatly improve your development work flows and releases. Next.js uses REACT for the front end, and nodejs for the back end therefore your entire project will be javascript. Since the entire project is in the same language it will make it easier for developers to be considered “full stack”. For today’s purposes we are going to discuss more about the front-end side and how to setup Next.js to track application state across any page in the Next.js application.
We will be demonstrating having a counter and modifier. The modifier will indicate how much to adjust the count by. We will then have pages that will either add, subtract, multiple, or divide the current count. No matter what page you are on the current count and current modifier will be the same until an action is executed.
At a high level. The three options to manage the state are:
- useState
- useReducer
- or use the Redux Toolkit
There is not a one size fits all approach so depending on the needs of your application it will depend on the strategy that you will deploy. We will discuss some of the pros and cons about these approaches along the way. Furthermore, sharing the state can get a little more complicated when you introduce TypeScript. But it is a best practice and we will go over that here. If you would like to follow along, we are going to create a git branch for each method.
To begin, choose a directory on your computer that will be used to contain the new next.js app by executing the following command. Of course, you can choose different names and adjust accordingly. You can also just read the rest of the article or my completed git repository and tell for yourself what bits of code are important to you.
To start from scratch execute the following command:
npx create-next-app --typescript
useState
Managing Next.js application state with useState
As you learn about React or Next.js you will commonly see examples of the useState being used in an individual component. Some other examples online will show you how to share that state with a child component. But what if this state is common across the entire application and nearly all pages or components need to know about the state? Well luckily you can use the useState that you have probably already learned about and once you add it to a context and create a provider you will be able to do that.
I would recommend creating a new branch immediately called “useState” so that we can always start each branch from the base next.js typescript template. You can use the UI in your IDE to do that or execute the following command.
git checkout -b useState
import React from 'react'
export interface IAppState {
currentCount: number
currentModifier: number
}
const useValue = () => {
const [appState, setAppState] = React.useState<IAppState>({currentCount: 0, currentModifier: 1});
return {
appState,
setAppState
}
}
interface IAppStateContext extends ReturnType<typeof useValue> {
}
export const AppStateContext = React.createContext<IAppStateContext>({} as IAppStateContext)
interface ContextProviderProps {
children: JSX.Element
}
export const AppStateContextProvider: React.FC<ContextProviderProps> = (props) => {
return (
<AppStateContext.Provider value={useValue()}>
{props.children}
</AppStateContext.Provider>
)
}
1 – What we are doing here is first specifying a model for our entire applications state IAppState. It has two number properties of currentCount and currentModifier. Next, we use React’s useState to set a default value and get back the current value, and the modifier function. We return those values in the useValue method.
2 – We then create an additional interface that extends the return type of the useValue. This makes it so that type will change even when we make changes to the IAppState interface without having to modify the AppStateContext interface. We then export the AppStateContext from React’s created context. For now, it is just an empty object.
3 – We then define the props that will be passed into our exported provider element. Create the provider element, and then encase the children within that component. This means that any component that will be wrapped within our AppStateContextProvider element will have access to our entire application state.
So far. So good. Now let’s create a very basic layout that has current count and current modifier at the top of the page. The modifier can be updated at will at the top of any page, and the count will be modified after pressing a button on any operations page. The layout will also have links to the operation pages (Add, subtract, etc).
In the “src” directory add a new directory called “components”. Create a new file called “Layout.tsx”.
Give it the following code:
import Link from "next/link"
import { AppStateContext } from '@/context/AppContext'
import { useContext } from "react"
interface LayoutProps {
children: JSX.Element
}
export default function Layout({children}: LayoutProps): JSX.Element {
const {appState, setAppState} = useContext(AppStateContext);
return(
<>
<div style={{
display: 'flex',
flexDirection: 'row'
}}>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/add">Add</Link>
</li>
<li>
<Link href="/subtract">Subtract</Link>
</li>
<li>
<Link href="/multiply">Multiply</Link>
</li>
<li>
<Link href="/divide">Divide</Link>
</li>
<li>
<Link href="/reset">Reset</Link>
</li>
</ul>
</div>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
Current Count: {appState.currentCount}<br />
Current Modifier: <input type="number" min="0" max="20" value={appState.currentModifier} onChange={(e) => {
setAppState({...appState, currentModifier: parseInt(e.target.value)})
}} /><br/>
{children}
</div>
</div>
</>
)
}
This truly is the basics of using this method of managing state. We import the Context, and then use values from the context, or set the values using the setter function that is returned from the useContext method of React.
Next, we will modify the App component so that it will have access to our AppStateContext and change the layout for all pages going forward.
In the “src” directory and the “pages” directory modify the _app.tsx file as follows:
// import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { AppStateContextProvider } from '@/context/AppContext'
import Layout from '@/components/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<AppStateContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppStateContextProvider>
)
}
Next, let’s update the index.tsx file in this same directory to the following code.
import Head from 'next/head'
export default function Home() {
return (
<>
<Head>
<title>Managing AppState with useState</title>
<meta name="description" content="Demonstrates managing entire app state with nextjs using react's built in useState method" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>Home</h1>
</header>
<main>
<p>Select an action on the left to modify the current count. You can update the modifier by using the spinner next to the current modifier on this page or any other page.</p>
<p>In this demonstration we are managing the entire app state using the useState method that comes with React.</p>
</main>
</>
)
}
npm run dev
Let’s go ahead and build our add page.
In the “src\pages” directory add a new file called “add.tsx”. Give it the following code.
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function AddPage() {
const { appState, setAppState } = useContext(AppStateContext);
return (
<>
<Head>
<title>Add</title>
</Head>
<header>
<h1>Add</h1>
</header>
<main>
<button onClick={() => {
setAppState({
...appState,
currentCount: appState.currentCount + appState.currentModifier
})
}}>Add {appState.currentModifier} to the count</button>
</main>
</>
)
}
Initial state:
subtract.tsx
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function SubtractPage() {
const { appState, setAppState } = useContext(AppStateContext);
return (
<>
<Head>
<title>Subtract</title>
</Head>
<header>
<h1>Subtract</h1>
</header>
<main>
<button onClick={() => {
setAppState({
...appState,
currentCount: appState.currentCount - appState.currentModifier
})
}}>Subtract {appState.currentModifier} from the count</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function MultiplyPage() {
const { appState, setAppState } = useContext(AppStateContext);
return (
<>
<Head>
<title>Multiply</title>
</Head>
<header>
<h1>Multiply</h1>
</header>
<main>
<button onClick={() => {
setAppState({
...appState,
currentCount: appState.currentCount * appState.currentModifier
})
}}>Multiy the current count by {appState.currentModifier}</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function DividePage() {
const { appState, setAppState } = useContext(AppStateContext);
return (
<>
<Head>
<title>Divide</title>
</Head>
<header>
<h1>Divide</h1>
</header>
<main>
<button onClick={() => {
setAppState({
...appState,
currentCount: appState.currentCount / appState.currentModifier
})
}}>Divide the current count by {appState.currentModifier}</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function ResetPage() {
const { appState, setAppState } = useContext(AppStateContext);
return (
<>
<Head>
<title>Reset</title>
</Head>
<header>
<h1>Reset</h1>
</header>
<main>
<button onClick={() => {
setAppState({
currentCount: 0,
currentModifier: 1
})
}}>Reset the current count to 0 and the current modifier to 1</button>
</main>
</>
)
}
Press CTRL+C to stop the npm run dev that was running earlier.
If you are following along then commit your changes, otherwise read on.
useReducer
Managing Next.js application state with useReducer
If you are following along switch back to main and then create a new branch called useReducer that is based on master. Either use your IDE’s UI to do so or these commands will work. Otherwise read on.
git checkout main
git checkout -b useReducer
Create a new folder in the src directory called “context”. Add a file called “AppContext.tsx”. Give it the following code.
import React from 'react'
export interface IAppState {
currentCount: number
currentModifier: number
}
const defaultAppState: IAppState = {
currentCount: 0,
currentModifier: 1
}
type IAction =
| { type: 'add' }
| { type: 'subtract' }
| { type: 'multiply' }
| { type: 'divide' }
| { type: 'reset' }
| { type: 'setModifier', payload: number }
Currently we have:
- An interface that represents our Application State
- An object that represents the default value of our Application State
- And some actions that we are going to send to our reducer function
Now let’s create our reducer function.
function reduer(state: IAppState, action: IAction): IAppState {
if(action.type) {
switch(action.type) {
case 'add': {
return { ...state, currentCount: state.currentCount + state.currentModifier }
}
case 'subtract': {
return { ...state, currentCount: state.currentCount - state.currentModifier }
}
case 'multiply': {
return { ...state, currentCount: state.currentCount * state.currentModifier }
}
case 'divide': {
return { ...state, currentCount: state.currentCount / state.currentModifier }
}
case 'reset': {
return defaultAppState
}
case 'setModifier': {
return { ...state, currentModifier: action.payload }
}
}
}
return state;
}
We are only halfway done. We need to now use this reducer and apply it to a context.
Add the following code.
const useValue = () => {
const [appState, updateAppStateDispatcher] = React.useReducer(reduer, defaultAppState);
return {
appState,
updateAppStateDispatcher
}
}
interface IAppStateContext extends ReturnType<typeof useValue> {
}
export const AppStateContext = React.createContext<IAppStateContext>({} as IAppStateContext)
interface ContextProviderProps {
children: JSX.Element
}
export const AppStateContextProvider: React.FC<ContextProviderProps> = (props) => {
return(
<AppStateContext.Provider value={useValue()}>
{props.children}
</AppStateContext.Provider>
)
}
Your entire AppContext.tsx file should have the following code.
import React from 'react'
export interface IAppState {
currentCount: number
currentModifier: number
}
const defaultAppState: IAppState = {
currentCount: 0,
currentModifier: 1
}
type IAction =
| { type: 'add' }
| { type: 'subtract' }
| { type: 'multiply' }
| { type: 'divide' }
| { type: 'reset' }
| { type: 'setModifier', payload: number }
function reduer(state: IAppState, action: IAction): IAppState {
if(action.type) {
switch(action.type) {
case 'add': {
return { ...state, currentCount: state.currentCount + state.currentModifier }
}
case 'subtract': {
return { ...state, currentCount: state.currentCount - state.currentModifier }
}
case 'multiply': {
return { ...state, currentCount: state.currentCount * state.currentModifier }
}
case 'divide': {
return { ...state, currentCount: state.currentCount / state.currentModifier }
}
case 'reset': {
return defaultAppState
}
case 'setModifier': {
return { ...state, currentModifier: action.payload }
}
}
}
return state;
}
const useValue = () => {
const [appState, updateAppStateDispatcher] = React.useReducer(reduer, defaultAppState);
return {
appState,
updateAppStateDispatcher
}
}
interface IAppStateContext extends ReturnType<typeof useValue> {
}
export const AppStateContext = React.createContext<IAppStateContext>({} as IAppStateContext)
interface ContextProviderProps {
children: JSX.Element
}
export const AppStateContextProvider: React.FC<ContextProviderProps> = (props) => {
return(
<AppStateContext.Provider value={useValue()}>
{props.children}
</AppStateContext.Provider>
)
}
Now let’s do what we did similar to before which is to create a common layout and add the context to the app so that every page can share in this application state.
To the “src” directory create a new directory called “components” and create a new file called “Layout.tsx”. Give it the following code.
import Link from "next/link"
import { AppStateContext } from '@/context/AppContext'
import { useContext } from "react"
interface LayoutProps {
children: JSX.Element
}
export default function Layout({children}: LayoutProps): JSX.Element {
const {appState, updateAppStateDispatcher} = useContext(AppStateContext);
return(
<>
<div style={{
display: 'flex',
flexDirection: 'row'
}}>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/add">Add</Link>
</li>
<li>
<Link href="/subtract">Subtract</Link>
</li>
<li>
<Link href="/multiply">Multiply</Link>
</li>
<li>
<Link href="/divide">Divide</Link>
</li>
<li>
<Link href="/reset">Reset</Link>
</li>
</ul>
</div>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
Current Count: {appState.currentCount}<br />
Current Modifier: <input type="number" min="0" max="20" value={appState.currentModifier} onChange={(e) => {
updateAppStateDispatcher({type: 'setModifier', payload: parseInt(e.target.value)})
}} /><br/>
{children}
</div>
</div>
</>
)
}
Modify the src/pages/_app.tsx file as follows:
// import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { AppStateContextProvider } from '@/context/AppContext'
import Layout from '@/components/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<AppStateContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppStateContextProvider>
)
}
Update the src/pages/index.tsx file as follows:
import Head from 'next/head'
export default function Home() {
return (
<>
<Head>
<title>Managing AppState with useReducer</title>
<meta name="description" content="Demonstrates managing entire app state with nextjs using react's built in useReducer method" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>Home</h1>
</header>
<main>
<p>Select an action on the left to modify the current count. You can update the modifier by using the spinner next to the current modifier on this page or any other page.</p>
<p>In this demonstration we are managing the entire app state using the useReducer method that comes with React.</p>
</main>
</>
)
}
npm run dev
Create a src/pages/add.tsx file with the following code.
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function AddPage() {
const { appState, updateAppStateDispatcher } = useContext(AppStateContext);
return (
<>
<Head>
<title>Add</title>
</Head>
<header>
<h1>Add</h1>
</header>
<main>
<button onClick={() => {
updateAppStateDispatcher({type:'add'})
}}>Add {appState.currentModifier} to the count</button>
</main>
</>
)
}
As in the previous example it is pretty much wash rinse and repeat from here only you will be changing some text on the page and the type of value for the updateAppStateDispatcher
src/pages/subtract.tsx
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function SubtractPage() {
const { appState, updateAppStateDispatcher } = useContext(AppStateContext);
return (
<>
<Head>
<title>Subtract</title>
</Head>
<header>
<h1>Subtract</h1>
</header>
<main>
<button onClick={() => {
updateAppStateDispatcher({type:'subtract'})
}}>Subtract {appState.currentModifier} from the count</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function MultiplyPage() {
const { appState, updateAppStateDispatcher } = useContext(AppStateContext);
return (
<>
<Head>
<title>Multiply</title>
</Head>
<header>
<h1>Multiply</h1>
</header>
<main>
<button onClick={() => {
updateAppStateDispatcher({type:'multiply'})
}}>Multiply the current count by {appState.currentModifier}</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function DividePage() {
const { appState, updateAppStateDispatcher } = useContext(AppStateContext);
return (
<>
<Head>
<title>Divide</title>
</Head>
<header>
<h1>Divide</h1>
</header>
<main>
<button onClick={() => {
updateAppStateDispatcher({type:'divide'})
}}>Divide the current count by {appState.currentModifier}</button>
</main>
</>
)
}
import Head from 'next/head'
import { AppStateContext } from "@/context/AppContext";
import { useContext } from "react";
export default function ResetPage() {
const { appState, updateAppStateDispatcher } = useContext(AppStateContext);
return (
<>
<Head>
<title>Reset</title>
</Head>
<header>
<h1>Reset</h1>
</header>
<main>
<button onClick={() => {
updateAppStateDispatcher({type:'reset'})
}}>Reset the current count to 0 and the current modifier to 1</button>
</main>
</>
)
}
As stated before this is another interesting way of managing state across the entire Next.js application. The advantage here is that the logic has been extracted to the reducer instead of needing to be determined by a setState method. Where it could get hairy is back in the AppContext.tsx file at line 13 where we are defining the IAction interface and basically specifying all of the different actions and payloads that action could receive. This interface could get really big and stop to be an advantage and become a hinderance. For example what if we wanted one reducer for the currentCount method, and then another reducer for the currentModifier portion of the state because right now we sort of have a mixed bag. Well, that’s where redux toolkit (rtk) comes in and is the option that we will be discussing next.
If you are following along go ahead and commit these files to your useReducer branch, then swap back out to main and create a new branch based on main called rtk.
Redux Toolkit (RTK)
Managing Next.js application state with Redux Toolkit (RTK)
In this case we will be bringing in a popular third party library called Redux Toolkit. Anyone who has done enough React programming has probably come across this before, and since Next is just a glorified version of React it should come as no surprise that we will be discussing redux for state management with Next.js.
To begin, execute the following commands to install the reduxtoolkit and react-redux
npm install @reduxjs/toolkit react-redux
This time I am going to take the approach of breaking things up in to “features” that usually has the component view of the state, the manager of the state as a slice and any other files that can assist with the feature in a single folder, then we will provide those slices to the store as you will soon see.
In this instance the “counter” is one feature, and the “modifier” is another feature.
In the “src” folder create a new folder called “features”. In the features folder create a new folder called “counter” in the “counter” folder create a new file called “counterSlice.ts”. Notice that this is not a tsx file because in this file we are not export a react element.
Give it the following code.
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit/dist/createAction";
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
add: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
We are imporing the createSlect from the redux toolkit as well as the PayloadAction type.
We are creating an interface that represents this slice of the state, then create a default value that matches the interface.
We are then using the createSlect function to create a slice of state called counter, giving it the initial state, and then specifying reducers. This is similar to the switch statement we did with the useReducer earlier except this time we are actually mutating the state instead of setting it in an unmutable way. This is because RTK has some things that detect state changes and returns them in an unmutable way. Later we will be able to extract each of these reducers as an action, and then export the main reducer that will be used by our components.
Update the code for your counterSlice.ts to the following:
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit/dist/createAction";
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
add: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
subtract: (state, action: PayloadAction<number>) => {
state.value -= action.payload
},
multiply: (state, action: PayloadAction<number>) => {
state.value *= action.payload
},
divide: (state, action: PayloadAction<number>) => {
state.value /= action.payload
},
reset: (state) => {
state.value = initialState.value
}
}
})
export const {add, subtract, multiply, divide, reset} = counterSlice.actions
export default counterSlice.reducer
To the features folder let’s create a new folder called “modifier”. In the modifier folder create a new file called “modifierSlice.ts”. Give it the following code.
import { createSlice } from "@reduxjs/toolkit"
import type { PayloadAction } from "@reduxjs/toolkit"
export interface ModifierState {
value: number
}
const initialState: ModifierState = {
value: 1
}
export const modifierSlice = createSlice({
name: 'modifier',
initialState,
reducers: {
setValue: (state, action: PayloadAction<number>) => {
state.value = action.payload
}
}
});
export const { setValue } = modifierSlice.actions
export default modifierSlice.reducer;
We will get into the UI portion of this later, but for now let’s work on bringing these “slices” in to one reducer / store.
In the src folder create a new folder called “store”. To the “store” folder create a new file called “index.tsx”. Give it the following code:
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from '../features/counter/counterSlice'
import modifierReducer from '../features/modifier/modifierSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
modifier: modifierReducer,
},
});
// Infer the RootState and AppDispatch types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Infer the type {counter: value:0, modifier: value: 1}
export type AppDispatch = typeof store.dispatch
In the other two examples our application state was:
{
"currentCount": 0,
"currentModifier": 1
}
{
"counter": {
"value": 0
},
"modifier": {
"value": 1
}
}
Before we get in to modifying the UI of the application let’s discuss something else. As you recall when we reset the state we were simply using a reducer or setState method that set both the currentCount and the currentModifier values at the same time. Well now we have a complication on our hands because in the Redux Toolkit world any slice of state is not aware of the other slice’s state. Instead it will have to be selected and provided as a payload to another slice. We will demonstrate some of that in the UI but before I do let’s show how we can reset but the counter and the modifier at the same time despite the fact that we are using slices.
Return to the modifierSlice.ts file.
To the top add the following import.
import {reset} from "@/features/counter/counterSlice"
Next, we will add an extraReducers property to our createSlect function with the following code:
extraReducers: {
[reset.type]: (
state
) => {
// When the counter resets then the modifier resets as well
state.value = initialState.value
}
}
Your entire modifierSlice.ts file should look like this:
import { createSlice } from "@reduxjs/toolkit"
import type { PayloadAction } from "@reduxjs/toolkit"
import {reset} from "@/features/counter/counterSlice"
export interface ModifierState {
value: number
}
const initialState: ModifierState = {
value: 1
}
export const modifierSlice = createSlice({
name: 'modifier',
initialState,
reducers: {
setValue: (state, action: PayloadAction<number>) => {
state.value = action.payload
}
},
extraReducers: {
[reset.type]: (
state
) => {
// When the counter resets then the modifier resets as well
state.value = initialState.value
}
}
});
export const { setValue } = modifierSlice.actions
export default modifierSlice.reducer;
To the src/features/modifier folder create a new file called “Modifier.tsx”. Give it the following code.
import React from 'react'
import type { RootState } from '@/store'
import { useSelector, useDispatch } from 'react-redux'
import { setValue } from './modifierSlice'
export function Modifier() {
const modifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<>
Modifier <input type="number" min="1" max="20" value={modifierValue} onChange={(e) => {
dispatch(setValue(parseInt(e.target.value)))
}} />
</>
)
}
To the src/features/counter folder let’s create a new file called “Counter.tsx” which will be used as the primary display component for our counter and its current value.
import React from "react"
import type {RootState} from "@/store"
import { useSelector } from 'react-redux'
export function Counter() {
const count = useSelector((state: RootState) => state.counter.value)
return(
<>
Current Count: {count}
</>
)
}
Since we broke this up in to feature’s let’s add the add, subtract, multiply, divide, and reset buttons to the feature, and we will import them on to the pages later.
In src/features/counter create a new file called AddButton.tsx and give it the following code:
import React from "react"
import type { RootState } from "@/store"
import { useSelector, useDispatch } from 'react-redux'
import { add } from './counterSlice'
export function AddButton() {
const currentModifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(add(currentModifierValue))}>
Add {currentModifierValue} to the current count
</button>
)
}
SubtractButton.tsx
import React from "react"
import type { RootState } from "@/store"
import { useSelector, useDispatch } from 'react-redux'
import { subtract } from './counterSlice'
export function SubtractButton() {
const currentModifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(subtract(currentModifierValue))}>
Subtract {currentModifierValue} from the current count
</button>
)
}
import React from "react"
import type { RootState } from "@/store"
import { useSelector, useDispatch } from 'react-redux'
import { multiply } from './counterSlice'
export function MultiplyButton() {
const currentModifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(multiply(currentModifierValue))}>
Multiply the current count by {currentModifierValue}
</button>
)
}
import React from "react"
import type { RootState } from "@/store"
import { useSelector, useDispatch } from 'react-redux'
import { divide } from './counterSlice'
export function DivideButton() {
const currentModifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(divide(currentModifierValue))}>
Divide the current count by {currentModifierValue}
</button>
)
}
import React from "react"
import type { RootState } from "@/store"
import { useSelector, useDispatch } from 'react-redux'
import { reset } from './counterSlice'
export function ResetButton() {
const currentModifierValue = useSelector((state: RootState) => state.modifier.value)
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(reset())}>
Reset the current count to 0 and the modifier to 1
</button>
)
}
Create a new folder called “components”, and a new file called “Layout.tsx” to that folder. Give it the following code:
import Link from "next/link"
import {Counter} from "@/features/counter/Counter"
import { Modifier } from "@/features/modifier/Modifier"
interface LayoutProps {
children: JSX.Element
}
export default function Layout({children}: LayoutProps): JSX.Element {
return(
<>
<div style={{
display: 'flex',
flexDirection: 'row'
}}>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/add">Add</Link>
</li>
<li>
<Link href="/subtract">Subtract</Link>
</li>
<li>
<Link href="/multiply">Multiply</Link>
</li>
<li>
<Link href="/divide">Divide</Link>
</li>
<li>
<Link href="/reset">Reset</Link>
</li>
</ul>
</div>
<div style={{
border: '1px solid black',
padding: '1rem'
}}>
<Counter /><br />
<Modifier />
{children}
</div>
</div>
</>
)
}
Now let’s provide the store and layout for every page in our application.
Modify the src/pages/_app.tsx file as follows.
// import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import {store} from '@/store'
import { Provider } from 'react-redux'
import Layout from '@/components/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<Layout>
<Component {...pageProps} />
</Layout>
</Provider>
)
}
Modify the src/pages/index.tsx file as follows:
import Head from 'next/head'
export default function Home() {
return (
<>
<Head>
<title>Managing AppState with Redux Toolkit (RTK)</title>
<meta name="description" content="Demonstrates managing entire app state with nextjs using Redux Toolkit (RTK)" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<header>
<h1>Home</h1>
</header>
<main>
<p>Select an action on the left to modify the current count. You can update the modifier by using the spinner next to the current modifier on this page or any other page.</p>
<p>In this demonstration we are managing the entire app state using the Redux Toolkit (RTK).</p>
</main>
</>
)
}
To the src/pages/Add.tsx file:
import Head from 'next/head'
import {AddButton as ActionButton} from '@/features/counter/AddButton'
const pageName = "Add"
export default function AddPage() {
return (
<>
<Head>
<title>{pageName}</title>
</Head>
<header>
<h1>{pageName}</h1>
</header>
<main>
<ActionButton></ActionButton>
</main>
</>
)
}
Subtract.tsx
import Head from 'next/head'
import {SubtractButton as ActionButton} from '@/features/counter/SubtractButton'
const pageName = "Subtract"
export default function SubtractPage() {
return (
<>
<Head>
<title>{pageName}</title>
</Head>
<header>
<h1>{pageName}</h1>
</header>
<main>
<ActionButton></ActionButton>
</main>
</>
)
}
import Head from 'next/head'
import {MultiplyButton as ActionButton} from '@/features/counter/MultiplyButton'
const pageName = "Multiply"
export default function MultiplyPage() {
return (
<>
<Head>
<title>{pageName}</title>
</Head>
<header>
<h1>{pageName}</h1>
</header>
<main>
<ActionButton></ActionButton>
</main>
</>
)
}
import Head from 'next/head'
import {DivideButton as ActionButton} from '@/features/counter/DivideButton'
const pageName = "Divide"
export default function DividePage() {
return (
<>
<Head>
<title>{pageName}</title>
</Head>
<header>
<h1>{pageName}</h1>
</header>
<main>
<ActionButton></ActionButton>
</main>
</>
)
}
import Head from 'next/head'
import {ResetButton as ActionButton} from '@/features/counter/ResetButton'
const pageName = "Reset"
export default function ResetPage() {
return (
<>
<Head>
<title>{pageName}</title>
</Head>
<header>
<h1>{pageName}</h1>
</header>
<main>
<ActionButton></ActionButton>
</main>
</>
)
}
If you have been following, along go ahead and commit these changes to your rtk branch.
Conclusion
Managing the state of your entire application is a very important and sensitive subject. Some things that we did not discuss earlier is just how much detail is needed for the global state and how much is needed just for the page? The page can still use its own useState or useReducer methods as necessary and can be used to augment the rest of the application state if that is indeed your intention. Be sure to plan out what is necessary for every page, and what is only necessary for the current page. For example, the layout or theme that a user selected is probably good to keep in the entire application state, as well as some of the login information if your application is having users log in to it. But data that will go on individual forms like contact us forms, or content management forms are probably not good ideas to keep as entire application state.
Going through this you might conclude that using redux toolkit is probably the way to go. And you would be right if you do indeed believe that your application state could get big and unwieldy. But remember we were able to just use react’s useState function for this same sample application. Even React Toolkit’s own documentation states that you should not use Redux Toolkit just because some one else said that you need to do so. There are tradeoffs. For example the first example is clear how the application state changes, when a button is pressed, and that might be more legible code for other developers that join your team. The second example shows how state can change when a certain action has been dispatched, but reading just that file alone might have you wondering when does that happen? On the other hand the trade off is that you will only have one spot to define the logic. The third example has allot of abstraction, and if you only have a small bit of state, this can be overkill. Consider what it took for us to be able to reset both the modifier and counter at the same time. Consider too just how many more files we created compared to the other examples as well. Organizing the code with features in such a way can make it easier to see how actions trigger reducers, but if you have your reducers being triggered outside of the feature folder, or you have many files in your feature folder it still could be difficult to understand for developers that are new to your team.
There is no one size fits all for state management, but I hope that experiencing these three different ways can help you consider your use case and guide you to which strategy to use. Happy coding.
About Intertech
Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.