A “state” is a JavaScript object which allows us to manage data and which is used to represent information about a component’s value. State management is handling your application’s UI and dictating what users see and what data is stored.
State management is a core aspect of any application. Understanding state management will help keep code centralized and performant. It is very integral to the point that there are dedicated hooks and open-source libraries made specifically to make it easier.
This article will enable us to understand state and how to manage it in Next.js using React hooks, Context API, Redux and Data Fetching.
While You Are at It, Why Not Learn More About The Newest Version of React – React 18
What Is State
A state is the internal value of a React component at any given point. It can contain various data types like arrays and objects. It can also be known as a dynamic store for managing application data. A state holds the current value of data. For example, a state can hold one value from a list of values coming from a server but might hold a different value when a button is clicked.
What Is State Management?
State management is the process of handling and maintaining knowledge of the state of an application. State management determines which data to show and how to show them as users interact with the application. This management usually handles the state of user interface control systems like input fields, CTA buttons, etc.
Some benefits of state management include centralizing and easy maintenance of code, making data handling easy, reducing code size, and doing away with dead code.
A Brief Look at the Next.js File Structure
Understanding the file structure in a Next.js file enables a better understanding of the different ways to store and handle state.
A new Next.js application consists of a pages, style and public folders. Inside the pages folder, which we will concentrate on in this article, we have two files: the index.js and the _app file, and an api folder. The _app file holds all globally accessible components, while the api folder contains the app’s API endpoint.
React Hooks for Managing State
Before introducing hooks in React V16.8, the state could only be accessed and managed using this.state variable, and the data type is always an object. The introduction of hooks took out the need for the this.state variable.
Hooks are functions that allow you to use React features without the need for writing classes. They “hook” into lifecycle features and will enable you to reuse logic without affecting component hierarchy.
This section looks at hooks for managing state in a Next.js application.
The useState Hook
The useState hook is the most famous and widely used. It allows you to keep track of the state of functional components. The useState hooks take any data type and hold a single value.
import { useState } from "react";
export default function Home() {
const [count, setCount] = useState(0);
const addCount = () => {
setCount(count + 1)
}
return (
<>
<h1>Your score is {score}</h1>
<button onСlick={addCount}>Add count</button>
</>
);
}
The code block above shows how to use the useState hook in a Next.js application. We first import useState from React. We initialize the state by calling it in the function component: the useState returns two values and the initial state, which is set to zero (0). The two values are the current state and a function that updates the state.
We created an addCount function which uses the state function updater to increase the initial value of the state by 1. We passed that function to the button as an onClick event whenever the button gets clicked.
The useReducer Hook
The useReducer hook, while not as famous or widely used as the useState hook, is another hook used to manage state in a Next.js application. While the useState is advised for simple states, the useReducer is preferred for more complex state logic and management.
The useReducer hook updates parts of the state when specific actions are dispatched. It works similarly to Redux, which we will discuss in later details of this article.
The useReducer requires two arguments and an optional third argument: the reducer function, the initial state or argument, and the initializer.
- Reducer: The reducer argument takes a state and an action and returns a new state based on the action. The reducer argument takes in two arguments. The state and the action
- The state is the current state of the application.
- The action holds details of the current action happening.
- Initial state: This holds the default value of the state.
- Initializer: The third value is optional and omitted in most cases. It allows you to extract logic for computing the initial state outside the reducer function. The argument exists in Redux. Since the useReducer hook is similar to Redux, it is a nice-to-have feature.
const [state, dispatch] = useReducer(reducer, initialArg, init);
The useReducer hook returns an array that holds the state and a dispatch function.
import { useReducer } from "react";
export default function Home() {
const [add, dispatch] = useReducer((state, action) => {
return state + action;
}, 0);
const [subtract, dispatch] = useReducer((state, action) => {
return state - action;
}, 10);
<>
<h3>{add}</h3>
<div>
<button onСlick={() => dispatch(1)}>
increment
</button>
<h3>{subtract}</h3>
<button onСlick={() => dispatch(1)}>
decrement
</button>
</div>
</>
);
}
In the code block above, we imported the useReducer hook. Then we initialized it by passing in the reducer and the initial state. The hook returned the current state and the dispatch.
We created two counters and then two buttons. When the increment button gets clicked, the counter increases by 1, while the decrement button decreases the decrement count by one. We passed the state to the counter and the dispatch function to the onClick events on the buttons.
Context API for Complex State
Using hooks in a Next.js application is all good and nice. A drawback that comes with that is the danger of prop drilling, especially in large projects.
Prop drilling is the expression given to the process of sending data or props down through many layers of nested children components from a high-level component to a lower-level component. Prop drilling is acceptable in smaller projects but can quickly become laborious in more significant and massive projects because of repeated code.
The React team introduced the Context API in React in React 16.3 to combat this prop drilling issue without installing external state managements like Redux.
The Context API works by providing all the values that need to be transmitted globally so that it is accessible to many components at different nested levels.
The Context API returns a provider and a consumer. The provider provides the state. It will hold the state and act as a parent while The consumer uses the state.
Context API can be used by following the steps below:
- Create a separate folder in the root directory and create a file with the name of your choice inside.
- Import the createContext inside
import { createContext, useState } from "react";
const AppContext = createContext()
- Create the Provider
const AppProvider = ({ children }) => {
const initialState = {
isLoggedIn: false,
isLoginPending: false,
loginError: null
}
const [state, setState] = useState(initialState);
return (
<AppContext.Provider value={{ state, setState }}>
{children}
</AppContext.Provider>
);
};
- Wrap the provider around the entire app
import { AppProvider } from "../where the context lives";
export default function MyApp({ Child, pageProps }) {
return (
<AppProvider>
<Child {...pageProps} />
</AppProvider>
);
}
The Context can now be consumed however you like
import Link from "next/link";
import { useContext } from "react";
import { AppContext } from "../where the context lives";
export default function Home() {
const { state } = useContext(AppContext);
return (
<>
if (!state.isLoggedIn)
return <Login />;
else
return <Dashboard />
}
</>
);
}
Redux
Redux was introduced at a crucial point and has long become one of the hottest topics in the frontend ecosystem. Redux is a state container for JavaScript applications that behave consistently. Redux handles applications state in an accessible store, and any component can assess the state it needs from the store without the need for passing props.
There are three parts to Redux: actions, store, and reducers.
Actions are ways of sending data from the application to the store. Actions carry a payload consisting of the information that the application should work on.
The store handles the application’s state. It is advised only to have one store in the application.
Reducers are functions that act as a connection between the actions and the store. Reducers take the current state of the application, perform the action and return a new state.
Managing the State With Data Fetching
For most of this article, our focus has been on internal state management. Most data will likely come from a third-party provider or API in a real-world application.
This data fetching process is not instantaneous, and there should be a way to keep track of and handle the state of the response.
Using the Fetch API and the useEffect Hook
The useEffect hook and Fetch API are one way to handle data fetching. The useEffect hook tells React that a component has to do something after being rendered. Those things can include fetching data, updating DOM and timers. They can also be known as side effects.
We can keep track of the state of the requested data and then render it when it appears.
import { useState, useEffect } from "react";
export default function Home() {
const [data, setData] = useState(null)
const [isLoading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch('// something from somewhere')
.then((res) => res.json())
.then((data) => {
setData(data)
setLoading(false)
})
}, [])
if (isLoading) return <h1>Loading...</h1>
if (!data) return <p>No data found</p>
return (
<>
<h1>Data requested:</h1>
<h2>{data?.title}</h2>
</>
)
}
We initialized separate states for received data in the code block above and tracked the fetch call’s state. We set the state of the fetch call to true and then create the fetching call, which happens immediately after the app renders for the first time. We track the state of the call and set it to false when the data arrives.
Understanding SWR (Stale-While-Revalidate)
The method created by the team behind Next.js to handle data fetching more conveniently is known as SWR.
SWR is a lightweight React hook library for data fetching. It works first by returning the data in the cache (stale), then sending the fetch request (revalidate); after that, it provides the up-to-date data for the application.
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
Conclusion
Managing state is not an easy job, but it improves workflow and better synchronizes the state of any application through multiple components. In this tutorial, we have taken a look at state and state management in a Next.js application. We also looked at different ways to manage state in a Next.js application.