How to use the useState hook in react properly. The article provides a deep dive into the proper usage of React's useState hook, highlighting best practices, common mistakes, and practical solutions.
React’s useState
hook is one of the most fundamental and commonly used hooks in functional components. While it simplifies state management, improper usage can lead to performance issues, unnecessary re-renders, and even unexpected behavior. In this article, we’ll explore the best practices for using useState
, common mistakes developers make, and how to avoid them with optimized approaches.
The useState
hook allows you to manage component-level state in a functional component. Here’s a simple example:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Here, count
is a state variable, and setCount
is a function that updates it.
useState
Bad Example:
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
setCount(count + 1); // This won't work as expected
};
Why is this bad? count
does not update synchronously; calling setCount
twice with count + 1
still results in only a single increment.
Fix: Use functional updates:
const increment = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1); // Now works as expected
};
Bad Example:
const [data, setData] = useState(fetchData()); // Fetching directly
Why is this bad? The function runs on every render, even when the state doesn’t change.
Fix: Use lazy initialization:
const [data, setData] = useState(() => fetchData());
This ensures fetchData()
runs only on the first render.
Bad Example:
const [name, setName] = useState("");
const handleChange = (event) => {
setName(event.target.value);
};
Why is this bad? Every keystroke triggers a state update and a re-render, even if the value hasn’t changed.
Fix: Update only if necessary:
const handleChange = (event) => {
const newValue = event.target.value;
setName((prev) => (prev === newValue ? prev : newValue));
};
Bad Example:
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data")
.then((response) => response.json())
.then(setData);
}, []);
Why is this bad? If the component unmounts before the fetch completes, calling setData
may trigger a state update on an unmounted component, leading to memory leaks.
Fix: Use a cleanup function:
useEffect(() => {
let isMounted = true;
fetch("/api/data")
.then((response) => response.json())
.then((data) => {
if (isMounted) setData(data);
});
return () => {
isMounted = false;
};
}, []);
Bad Example:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
Why is this bad? If firstName
and lastName
are often updated together, it’s inefficient to manage them separately.
Fix: Use an object for related state:
function UserForm() {
const [user, setUser] = useState({ firstName: "", lastName: "" });
const handleChange = (event) => {
setUser((prev) => ({ ...prev, [event.target.name]: event.target.value }));
};
const { firstName, lastName } = user;
return (
<div>
<input name="firstName" value={firstName} onChange={handleChange} />
<input name="lastName" value={lastName} onChange={handleChange} />
</div>
);
}
Bad Example:
const [numbers, setNumbers] = useState([1, 2, 3]);
const addNumber = () => {
numbers.push(4); // Direct mutation
setNumbers(numbers);
};
Why is this bad? React does not detect direct mutations, leading to unexpected behavior.
Fix: Always create a new array:
const addNumber = () => {
setNumbers((prev) => [...prev, 4]);
};
Bad Example:
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // Uses stale count value
}, 1000);
};
Fix: Use functional updates:
const handleClick = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
};
When passing state as props to child components, re-renders can occur if the parent component updates.
Bad Example:
const Parent = () => {
const [count, setCount] = useState(0);
return <Child count={count} />;
};
If Parent
re-renders for any reason, Child
will also re-render unnecessarily.
Fix: Use React.memo
to prevent unnecessary renders:
const Child = React.memo(({ count }) => {
return <p>Count: {count}</p>;
});
This ensures Child
only re-renders when count
actually changes.
Understanding useState
is crucial for writing efficient and bug-free React applications. By following best practices and avoiding common pitfalls, you can optimize state management and improve performance.
Join Bossadi on Peerlist!
Join amazing folks like Bossadi and thousands of other people in tech.
Create ProfileJoin with Bossadi’s personal invite link.
0
5
0