State Management
This section discusses one of the most important concepts of react, which is state management, without which the application or the library itself wouldn’t be called react. We will learn what state is, how state is managed in react, best practices in managing state and what other libraries can we use to manage state in react. Also, we will try to provide examples to further solidify the concept of state management in react. Let’s begin.
However, you can also read Part 1 on props & component, and Part 2 on Component Composition
In react, state simply refers to changes that occurred as a result of user interaction with the UI’s, server responses, or any dynamic event. Moreover, it also refers to the situation a component is along-side its children. This simply implies that a mechanism exists within react application or component that keep track of any changes, and that’s why it’s sometimes considered as the memory of a component that hold its data that’s likely to change. These changes could be as a result of changes in props data, UI interaction by user or server responses.
In react, a component can be in different state, notably are:
Moreover, there are other states that a component can have such as loading state (for indicating the process of fetching data or processing), error state for indicating an error in processing) UI state (used to track the display or visibility of UI elements), empty state, derived state (state extracted from another state), etc.
How is state manage in react?
As mentioned above, there are varieties of ways to manage state in react, either with built-in state management API or third-party libraries. This section demonstrates how the built-in features are used to manage state. With this, we will consider two hooks, namely useState and useReducer hook.
The useState hook
This hook is the most basic built-in state management API provided by react to manage state. The hook returns two pieces of information, the state variable use to represent the state and an updater function for updating the state variable. What does this mean? Let’s see it in practice.
const [name, setName] = useState(“”)
name refers to the state variable, setName is the updater function. The state variable is initialized with a value of empty strings (not always, only for this example) as contained in the argument passed to the useState hook. However, when we want our state variable to take an initial value, we pass the initial value as an argument to the useState hook. It’s worth mentioning that the declaration is always before the return statement of a functional component.
Consider a simple react application that calculates the age of any user. With this, we will have an input field that accepts the year of birth from the user and a button that when clicked, will display the age (in years) of the user.
// PersonAge.jsx
import { useState } from “react”;
function PersonAge(){
const [yearOfBirth, setYearOfBirth] = useState(0);
const [userAge, setUserAge] = useState(null);
const currentYear = 2024;
function handleClick(e) {
e.preventDefault();
const age = currentYear - yearOfBirth
setUserAge(age);
}
return (
<div>
<div>
<input
type=”number”
value={yearOfBirth}
onChange={(e) => setYearOfBirth(e.target.value)}
/>
<button onClick={handleClick}>Calculate Age</button>
<div>
{userAge && <p>Your age is: {userAge}</p>}
</div>
)
}
export default PersonAge;
In the above code snippet, we started by importing the useState hook from react. We defined?a component called PersonAge which returns an input field with a button captioned Calculate Age. Before the return statement, we created two pieces of state, each with a state variable (yearOfBirth and userAge) and an updater function (setYearOfBirth and setUserAge) respectively. The yearOfBirth was initialized with a value of zero and used as the attribute value in the input’s value field (something referred to as controlled input). The setYearOfBirth function was used within the onChange (an event handler) function (as an arrow function) body to set value for the yearOfBirth variable. Similarly, the userAge variable was initialized as null, but will change depending on the result of calculating (currentYear – yearOfBirth) the user’s age, which is passed to the setUserAge function. The userAge value was conditionally rendered depending on its value, if it’s a truth value, the paragraph element and its content are rendered to the screen.
We mentioned controlled input in the above paragraph. What does it mean? It refers to elements whose value is controlled by a state, like useState, and any changes or update are managed specifically by the updater function. If you notice the input element previously, the event handler (onChange) manages the state variable (yearOfBirth).
At very basic, this is how we use useState hook in react. It’s also worth knowing that the initial value can be of any data type including array and object, though with added complexity. To make life simple in managing complex state, useReducer can also come to the rescue as discussed below.
The useReducer hook
useReducer hook is yet another state management tool you can leverage to manage complex state. We earlier mentioned that useState can be used to manage a local state where the updater function can take different values as its initial values. The initial values can be of type array or object. While that is possible and acceptable, the process of updating the state variable tends to be challenging, especially for novice in react, and even experienced react developers need to pay attentive attention, otherwise, it can lead to unwanted behavior or prone to errors. To avoid such issues, useReducer provides means for efficiently and effectively managing states. How does it work?
It maintains a reducer function (like a store, as the only source of truth) where the state logic is been managed. The logic, refers to any operation for managing the state. To get clearer picture of it, let’s look at the syntax
const [state, dispatch] = useReducer(reducer, initialState)
The useReducer as seen above takes two arguments mostly, the reducer and the initialState. The reducer as mentioned earlier is where the state logic is managed and the initialState refers to the initial values of the state variable. Similarly, the useReducer returns two things similar to what useState returns, but performed somewhat differently. The state is the pointer to the variables initially initialized in the initialState, while the dispatch (though can be named anything), as the name implies, dispatches action that will be handled by the reducer accordingly. To fully demonstrate how useReducer works, we will implement a simple application that manages notes by users. We will implement simple CRUD (Create, Read, Update, and Delete) operations for the note application. However, we will focus on the logic of useReducer and the link for the complete application can be found on my GitHub account.
Start by creating a react application using either vite or create-react-app tool. Once you are done, within the src folder, create a folder called components. Inside the components folder, create two more files named Notes.js and NoteItem.js.
Next, within the Notes.js file, create a component called Note that returns a simple text field and a button titled Add. As earlier mentioned, the useReducer takes two arguments, the reducer and the initial state. The reducer and the initial state will be created outside the Notes component as defined below.
领英推荐
// InitialState
const initialState = {
notes: [
{
id: 1,
title: “Learning the concept of React”
},
{
id: 2,
title: “Learn Back-end with Node JS”
},
{
id: 3,
title: “Learn React Compound Component”
}
]
}
The initialState is an object with a single element called notes which is also an array of objects. The objects contain the id and title of every note.
Next, we define the reducer function, where the logic of CRUD operation will be performed as shown below.
Function reducer(state, action) {
switch(action.type) {
case “create”:
return {
...state,
notes: [...state.notes, {id: uuidv4(), title: action.payload}]
}
case “read”:
return {...state }
case “update”:
return {
...state,
notes: state.note.map((note) => {
if (note.id === action.note.id) {
return {...note, title: action.note.title};
}
return note;
}
}
case “delete”:
return {
...state,
notes: [
...state.notes.filter(note) => note.id !==action.payload)
]
};
default:
break;
}
}
Don’t panic, I will break it down as follows. We defined a reducer function that takes two arguments, a state and an action. The state is like a reference to the variables initialized with the initial state. The action has access to the type of event that will be performed and the data that will be use to manipulate the state variables. Within the reducer function, we used the switch statement to indicate the various cases (events, equivalent to the CRUD operations) that will be performed.
In the first case of creating a note, we set the case to “create” and in our return statement, we returned an object since the initialState is also an object. The use of ...state, signifies that we are returning everything in the initialState (something in JavaScript formally referred to as spread operation. The three dots before the state are known as spread operator). After the spreading the existing initialState, we update the notes by returning all notes (...state.notes) as an array and then adding a new note object with id and title. The id uses the uuidv4 to assign unique id to each note created, and the title got its value from the input field we created.
As for reading all notes, we also created a case as “read” that simply returns everything in the initialState, which is also indicated as {...state }. Ordinarily, that’s not much required.
The process of updating a note involves searching through the entire note to identify a note that matches a certain criteria (with an id), and if found, updates the note title accordingly. For this, we created a case named “update” that equally returns everything in the initialState (...state). Since the goal is to update a note title, we iterated over the notes using the map function and checked every note’s id against the received data (id, and title for update), and if found, we spread the note details and updates only the title (return {...note, title: action.note.title}; ), and then finally return the updated note.
Deleting a note involves removing a note on the screen since we are not using a database. Therefore, to perform the operation of deleting a note, we also created a case named “delete” and return everything in the initialState with ...state. We then iterated over the notes and filtered out (removed) a note whose id matches the received id from the dispatch function.
Finally, as a good practice, you always provide a default case in the event no case match the dispatched case from the dispatch function.
Having discussed the reducer function, we will now see how does the dispatch function pass data to the reducer function. This is achieved in react through creating event handlers as function in react as discussed below.
Dispatching Actions
To handle the case of creating a new note, we will create a function called handleAdd that takes an event as an argument. The handleAdd calls the dispatch function with an object containing a type attribute equal to create (same as the case name for creating a note) and a payload that holds the value for the new note title. In our example, we assigned the value of the payload to a state variable of useState.? Finally, we set the updater function to empty strings to clear the note field.
function handleAdd(e) {
e.preventDefault();
dispatch({
type: “create”,
payload: noteToAdd
});
}
Also, to trigger the deletion of a note, we created a handleDelete function which takes a single argument, the note id.? We called the dispatch function and specified the type as “delete” and the payload as the passed id. Upon received by the reducer function as explained above, the id will be use to filter out the note that is equal to the id.
function handleDelete(id) {
dispatch({
type: “delete”,
payload: id
});
}
For updating a note, a handleUpdate function was also created that calls a dispatch function, and pass the type as “update” and payload (but was called note) which is also an object with id set to noteId (the note’s id to be updated) and title set to the value of the note currently in the input field. This will be much more clear later. Finally, the handleUpdate sets the editing state back to false and cleared the input’s field after successful updates as shown below.
function handleUpdate() {
dispatch({
type: “update”,
note: {
id: noteId,
title: noteToAdd
}
setIsEditing(false);
setNoteToAdd(“”);
});
}
Finally, we created another handler function called handleEdit, though it doesn’t use the dispatch function, but used to set some states once the edit button is clicked by the user as shown below. It also filtered the note that matches with the id passed as an argument to the handleEdit.
function handleEdit(id) {
setIsEditing(true);
const [noteToEdit] = state.notes.filter((note) => note.id === id);
setNoteId( noteToEdit.id);
setNoteToAdd(noteToEdit.title);
}
Handling the states variables
We will now focus on how we access the state variables, that’s the variables we initialize in our initialState and passed to the reducer function. Wherever in our application we want to access the variables, we use the: state dot the name of the variable we want to access (since in our example the initial state is an object, state.notes), and that’s why in our note application, to display the notes, we map through and return a single note item as shown below
{
state.notes.map((note) => (
<NoteItem
key={note.id}
note={note}
handleDelete={handleDelete}
handleEdit={handleEdit}
/>
))}
what did we do? First, we entered into JavaScript mode using the curly braces ({}). In between, we accessed the state variable (note) and map through it which returns a note Item. For each note item, we passed a key, the note, an event handlers (for deleting and editing), which are appropriately handled by the NoteItem component.
And that’s it, in this section, we looked what useReducer is and how we can use it to manage state. This is just a basic idea to get you started with using useReducer. Even in this application, the management of the state can be improved, but our goal is to keep things simple.
In the second part of state management, we will see how we can use useContext API to manage state in react applications.