In the beginning articles, we learned about managing the component state using the useState()
hook. We understood the concept with code examples. Assuming that you are clear on using the useState()
hook for basic state management, I will move forward with managing the complex state updates in React with the same useState()
hook. It uses the simple state variable to manage the component's state and allows it to react to user interactions. However, with the increase in the complexity of your component, managing the state becomes more complex. In this article, you will learn how to manage the complex state changes with the useState()
hook.
useState Hook – A Brief Recap
Basically, the useState()
hook enables components to maintain their internal data, which is called state. When this hook is invoked with an initial value, it returns an array of two elements, i.e., the current state and the function to update that state.
To understand this, let's see a simple example. Suppose we have a counter that increases the value of a variable when a button is clicked. Create a new file named Counter.js and paste the following code into it:
import React, { useState } from 'react';
function Counter() {
const [counter, countSetter] = useState(0); // Initialize count to 0
const increment = () => {
countSetter(counter + 1); // Update the count state
};
return (
<div>
<p>Count: {counter}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
In the above code, we have created a simple Counter component. It manages its state using the state variable, i.e., counter. Initially, the useState()
hook is set to 0, meaning that the counter will start from 0. As the user clicks on the button, the value of the state variable is incremented and updated dynamically. The function countSetter
re-renders the component as its state changes. To see the above counter in action, call the Counter component in App.js.
The Functional Update: Updating State Based on the Previous State
As the component grows, we often need to update a component's state based on its previous state. In such cases, updating the state directly can lead to unexpected behaviors in asynchronous scenarios or when the React batch updates multiple states. Therefore, functional updates ensure that we are working with the latest state.
The Functional Update is a process where, instead of sending the new state value directly to the update function, we pass a function that contains the previous state as the argument and returns the new state.
To understand this, let's create another file named NewCounter.js. Add the following code to it:
import React, { useState } from 'react';
function NewCounter() {
const [counter, countSetter] = useState(0);
const increment = () => {
countSetter(countPrevious => countPrevious + 1); // Functional update
};
const decrement = () => {
countSetter(countPrevious => countPrevious - 1); // Another functional update
};
const incrementByTen = () => {
for (let i = 0; i < 10; i++) {
countSetter(countPrevious => countPrevious + 1); // Multiple functional updates
}
};
return (
<div>
<p>Count: {counter}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={incrementByTen}>Increment by 10</button>
</div>
);
}
export default NewCounter;
In the above code, I have defined 3 functions, each having its own purpose. The increment()
function receives the existing state as an argument and increases the value of the state variable by 1 every time the user clicks the Increment button. Similarly, the decrement()
function receives the current state as an argument and decreases the value of the state variable by 1 every time the user clicks the Decrement button. Finally, the IncrementByTen()
method receives the current state and adds 10 to it each time the user clicks the corresponding button. To see this in action, call the component in App.js.
Managing Multiple Related State Variables
With the increase in the complexity of the components, you might need to manage multiple states for related information. Although you can handle that using the individual useState()
hook for each state variable, grouping the related states into a common object is a better practice.
To understand this, consider that we have a form in which the user enters their name, email, and age. Let's group these together in a 'user' state variable and manage their states together. Create a new file named MultipleStates.js and add the following code to it:
import React, { useState } from 'react';
function MultipleStates() {
const [user, userSetter] = useState({
name: '',
age: '',
email: '',
});
const changeHandler = (event) => {
const { name, value } = event.target;
userSetter(userPrevious => ({
...userPrevious,
[name]: value,
}));
};
return (
<form>
<div>
<label>Name:</label>
<input type="text" name="name" value={user.name} onChange={changeHandler} />
</div>
<div>
<label>Age:</label>
<input type="number" name="age" value={user.age} onChange={changeHandler} />
</div>
<div>
<label>Email:</label>
<input type="email" name="email" value={user.email} onChange={changeHandler} />
</div>
<div>
<h3>Current Form Data:</h3>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Email: {user.email}</p>
</div>
</form>
);
}
export default MultipleStates;
In the above code, I have combined the 3 state variables, i.e., name, age, and email, into one object named user. The changeHandler()
function updates the object's state when the user enters a value on the textboxes.
More Complex Updates
There comes a time when a single interaction triggers updates across multiple related state variables. While this can be managed by making multiple userSetter()
calls, you can achieve more efficiency by consolidating these updates within a single functional update. Let's support, we have a shopping app with a shopping cart feature. When the user adds a certain number of products in the cart, they get a discount. In this situation, we have 3 states to manage, i.e., the number of items in the cart, the total bill of the customer, and a Boolean state that determines if the discount is applied or not. To see this in action, let's create a new file named ShoppingCart.js and add the following code to it:
import React, { useState } from 'react';
function ShoppingCart() {
const [cart, cartSetter] = useState({
items: [],
total: 0,
onDiscount: false,
});
const insertItem = (item) => {
cartSetter(previousCart => ({
...previousCart,
items: [...previousCart.items, item],
total: previousCart.total + item.price,
onDiscount: previousCart.items.length + 1 >= 3 || previousCart.onDiscount,
}));
};
return (
<div>
<h2>Your Shopping Cart</h2>
<ul>
{cart.items.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
<div>
<p>Total: ${cart.total.toFixed(2)}</p>
<p>Discount Applied: {cart.onDiscount ? 'Yes' : 'No'}</p>
</div>
<button onClick={() => insertItem({ id: 1, name: 'Apple', price: 10 })}>
Add Apple
</button>
<button onClick={() => insertItem({ id: 2, name: 'Banana', price: 20 })}>
Add Banana
</button>
<button onClick={() => insertItem({ id: 3, name: 'Mango', price: 15 })}>
Add Mango
</button>
</div>
);
}
export default ShoppingCart;
In the above code, I have updated the state of all the variables simultaneously using the single functional update, reducing the code size and enhancing the code efficiency. Hopefully, with these simple and basic examples, you were able to understand how to manage complex state changes with the functional updates using the useState()
hook. Try creating more applications for practice and implementing the concepts you have learned in this article.