React useState hook Mistakes You Must Avoid!

Rajesh Bhattarai
9 min readDec 18, 2022

In this blog, We will discuss the top 5 common useState hook mistakes that React developer makes. We will see all the common mistakes step by step and learn how to fix them with examples.

1. Depending on the API response to fill the state

I have seen many React developers define the useState hook without initializing it properly and later add the data with an API response. This is going to give you an error when you try to access the state data before the component renders because getting an API response is going to take some time.

Let us see an example below

Initialize Component

import React, { useEffect, useState } from 'react';

const Initialize = () => {
const [product, setProduct] = useState();

useEffect(() => {
// Consider this as an API response
const apiResponse = {
name: "T-Shirt",
price: "$399",
description: "A super coll T-Shirt for you"
}
setProduct(apiResponse);
}, [])
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
<p>{product.description}</p>
</div>
);
};

export default Initialize;

The Output

useState hook gives cannot read properties of undefined error

We are accessing the name property from the product state which is not there in the state. It gets added later once the API call is successful. So during the rendering of the component the product state is undefined. One good way to fix this error is to initialize the state.

Initializing the state in Initialize Component

import React, { useEffect, useState } from 'react';

const Initialize = () => {
const [product, setProduct] = useState({
name: "",
price: "",
description: ""
});

useEffect(() => {
// Consider this as an API response
const apiResponse = {
name: "T-Shirt",
price: "$399",
description: "A super coll T-Shirt for you"
}
setProduct(apiResponse);
}, [])
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
<p>{product.description}</p>
</div>
);
};

export default Initialize;

The Output

No Error in the console

After initializing the state, the error is gone from the console and we can clearly see the data on the web page.

2. Creating a state change method for each input element

Let's just say you have a form component where multiple input elements are present. Now you need to get the value from those input elements and update your component state. In this case, some Junior developers create a unique handle change function for each input element which is not a good way to handle the form data.

Let us see an example below

UpdateState component

import { useState } from "react";

function UpdateState() {
const [formData, setFormData] = useState({name: '', email: '', password: ''});

const handleName = event => {
setFormData({...formData, name: event.target.value})
}

const handleEmail = event => {
setFormData({...formData, email: event.target.value})
}

const handlePassword = event => {
setFormData({...formData, password: event.target.value})
}

const handleSubmit = () => {
console.log(formData);
}

return (
<div>
<div className="input-control">
<label htmlFor="name">Enter your Name</label>
<input
type="text"
className="input-field"
placeholder="Enter your name"
id="name"
onChange={handleName}
/>
</div>
<div className="input-control">
<label htmlFor="email">Enter your Email</label>
<input
type="email"
className="input-field"
placeholder="Enter your email"
id="email"
onChange={handleEmail}
/>
</div>
<div className="input-control">
<label htmlFor="password">Enter your Password</label>
<input
type="password"
className="input-field"
placeholder="Enter your password"
id="password"
onChange={handlePassword}
/>
</div>

<div>
<button onClick={handleSubmit} className="submit">Submit</button>
</div>

</div>
);
}

export default UpdateState;

As I said this is not a good way to handle form data. We need to have just one single function which is going to receive the data from all the input fields and update the state. Let us look at the below code and see how can we do that.

UpdateState component

import { useState } from "react";

function UpdateState() {
const [formData, setFormData] = useState({name: '', email: '', password: ''});

const handleChange = name => event => {
setFormData({...formData, [name]: event.target.value})
}

const handleSubmit = () => {
console.log(formData);
}

return (
<div>
<div className="input-control">
<label htmlFor="name">Enter your Name</label>
<input
type="text"
className="input-field"
placeholder="Enter your name"
id="name"
onChange={handleChange("name")}
/>
</div>
<div className="input-control">
<label htmlFor="email">Enter your Email</label>
<input
type="email"
className="input-field"
placeholder="Enter your email"
id="email"
onChange={handleChange("email")}
/>
</div>
<div className="input-control">
<label htmlFor="password">Enter your Password</label>
<input
type="password"
className="input-field"
placeholder="Enter your password"
id="password"
onChange={handleChange("password")}
/>
</div>

<div>
<button onClick={handleSubmit} className="submit">Submit</button>
</div>

</div>
);
}

export default UpdateState;

The Output

logging out the form data after clicking submit button

3. Rendering the component while updating input data

You can see in the above example I have used the onChange method in every input element which is going to update the state. And we know once the state is updated the component re-renders to reflect new changes. The problem with this is every time we input a character in an input field our component re-renders. This unnecessary re-rendering of components can be avoided using the useRef hook. If you want a detailed explanation about the useRef hook and forwardRef then Click Here.

Let us see an example below

UseRefImplement component

import React, { useRef } from 'react';

const UseRefImplement = () => {
const formField = useRef(null);

const handleSubmit = () => {
console.log(formField.current['name'].value);
console.log(formField.current['email'].value);
console.log(formField.current['password'].value);
}
return (
<div>
<form ref={formField}>
<div className="input-control">
<label htmlFor="name">Enter your Name</label>
<input
type="text"
className="input-field"
placeholder="Enter your name"
id="name"
name="name"
/>
</div>
<div className="input-control">
<label htmlFor="email">Enter your Email</label>
<input
type="email"
className="input-field"
placeholder="Enter your email"
id="email"
name="email"
/>
</div>
<div className="input-control">
<label htmlFor="password">Enter your Password</label>
<input
type="password"
className="input-field"
placeholder="Enter your password"
id="password"
name="password"
/>
</div>
</form>

<div>
<button onClick={handleSubmit} className="submit">Submit</button>
</div>

</div>
);
};

export default UseRefImplement;

The Output

logging out the form data after clicking submit button

4. Updating state with the async method

In this case, let us directly look at the example first.

AsyncMethod component

import React, { useEffect, useState } from 'react';

const AsyncMethod = () => {
const [count, setCount] = useState(0);

const increment = () => {
console.log("state updated");
setCount(count+1);
}

const asyncIncrement = () => {
setTimeout(() => {
setCount(count+1)
console.log("Async state updated");
}, 3000);
}

useEffect(() => {
console.log(count);
}, [count]);

return (
<div className="async">
<h2>{count}</h2>
<div className="button-group">
<button onClick={increment}>Increment</button>
<button onClick={asyncIncrement}>Async Increment</button>
</div>
</div>
);
};

export default AsyncMethod;

The Output

state update and async state update

Look at the console carefully. I have increased the number count to 7 by updating the state and now out of nowhere, you see 4 with an async state update.

What is wrong with the Async state update?

Actually, I increased the number count to 3 by clicking on the Increment button, and then I clicked on the Async Increment button. But you know the async function has setTimeout in it to mimic the async update. Now in the meantime, I increased the number count to 7 by clicking on the Increment button. And by this time the async state update got triggered and now according to the async method current state count is 3 because that's the count when I clicked on the async Increment. And hence it updates the count to 4 instead of 8.

This problem can be solved if the async state update method has a track of the current state. And we can do that by passing the callback function to the set state method

AsyncMethod component

import React, { useEffect, useState } from 'react';

const AsyncMethod = () => {
const [count, setCount] = useState(0);

const increment = () => {
console.log("state updated");
setCount(count+1);
}

const asyncIncrement = () => {
setTimeout(() => {
setCount(prevCount => prevCount + 1)
console.log("Async state updated");
}, 3000);
}

useEffect(() => {
console.log(count);
}, [count]);

return (
<div className="async">
<h2>{count}</h2>
<div className="button-group">
<button onClick={increment}>Increment</button>
<button onClick={asyncIncrement}>Async Increment</button>
</div>
</div>
);
};

export default AsyncMethod;

The Output

state update and async state update

And now you can see the issue is fixed just by passing the callback function to the set state method which has a track of the latest state data.

Updating the derived state

One of the most common mistakes the junior React developer makes is when there are two states in the component and the second state is derived from the first state then while updating the state they create a bug unknowingly.

Let us see an example below

DeriveState component

import React, { useState } from 'react';

const DeriveState = () => {
const [products, setProducts] = useState([
{
name: "T-Shirt",
price: "$399",
description: "A super cool T-Shirt for coders",
selectedColor: "crimson",
id: 146
},
{
name: "Shoes",
price: "$399",
description: "A great pair of shoes for your casual use",
selectedColor: "crimson",
id: 790
}
])

const [cartProduct, setCartProduct] = useState();

const addToCart = (productId) => {
const product = products.find(product => product.id === productId);
setCartProduct(product);
}

const handleColor = (color, productId) => {
setProducts(currentProducts => currentProducts.map(product => {
if (product.id === productId) {
return {...product, selectedColor: color}
}
return product
}));
}

return (
<div className="cards">
{
cartProduct ?
<div className="card-added">
{cartProduct.selectedColor} {cartProduct.name} is added to cart
</div> : null
}
{
products.map(product => (
<div key={product.id} className="card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<p>{product.price}</p>
<p className="quantity">
<button onClick={() => addToCart(product.id)} className="cart-btn">
Add To Cart
</button>
<span
onClick={() => handleColor("crimson", product.id)}
className="select-color crimson">+</span>
<span
onClick={() => handleColor("green", product.id)}
className="select-color green">+</span>
<span
onClick={() => handleColor("violet", product.id)}
className="select-color violet">+</span>
<span
onClick={() => handleColor("black", product.id)}
className="select-color black">+</span>
</p>
</div>
))
}
</div>
);
};

export default DeriveState;

The Output

The UI of Derive state component

You might be thinking hey that’s great all the functionality works what is the issue with the above code? Well, let me tell you there is a bug. If you click on the color buttons and try to update the color then the newly selected color doesn’t get reflected because here we have two different states and we are updating the only original one. The derive state is not updated.

In order to fix this bug we basically instead of storing the copy of the selected product in the derived state. We will store the id of the selected product. Look at the code below.

DeriveState component

import React, { useState } from 'react';

const DeriveState = () => {
const [products, setProducts] = useState([
{
name: "T-Shirt",
price: "$399",
description: "A super cool T-Shirt for coders",
selectedColor: "crimson",
id: 146
},
{
name: "Shoes",
price: "$399",
description: "A great pair of shoes for your casual use",
selectedColor: "crimson",
id: 790
}
])

const [cartProduct, setCartProduct] = useState();
const [cartProductId, setCartProductId] = useState();
const addedProductToCart = products.find(product => product.id === cartProductId);

const addToCart = (productId) => {
setCartProductId(productId);
}

const handleColor = (color, productId) => {
setProducts(currentProducts => currentProducts.map(product => {
if (product.id === productId) {
return {...product, selectedColor: color}
}
return product
}));
}

return (
<div className="cards">
{
addedProductToCart ?
<div className="card-added">
{addedProductToCart.selectedColor} {addedProductToCart.name} is added to cart
</div> : null
}
{
products.map(product => (
<div key={product.id} className="card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<p>{product.price}</p>
<p className="quantity">
<button onClick={() => addToCart(product.id)} className="cart-btn">
Add To Cart
</button>
<span
onClick={() => handleColor("crimson", product.id)}
className="select-color crimson">+</span>
<span
onClick={() => handleColor("green", product.id)}
className="select-color green">+</span>
<span
onClick={() => handleColor("violet", product.id)}
className="select-color violet">+</span>
<span
onClick={() => handleColor("black", product.id)}
className="select-color black">+</span>
</p>
</div>
))
}
</div>
);
};

export default DeriveState;

The Output

The UI of Derive state component

Now we can see the selected color on the page. We also removed the copy selected product and just stored the id of it.

Thank you for reading this far. I hope now you understood the common mistakes React developer makes when comes to using the useState hook. And you are ready to avoid these mistakes in your next project.

If you want more such content then do follow me on Medium and subscribe to my YouTube channel

Any doubt? reach me on Twitter

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

Interested in scaling your software startup? Check out Circuit.

We offer free expert advice and bespoke solutions to help you build awareness and adoption for your tech product or service.

--

--

I am a software developer who loves writing blogs about programming and web development.