Introduction
One of the most common problems React developers face is this: data is fetched correctly from an API, but the component UI does not update as expected. This usually happens after an asynchronous call, such as fetching data from a server.
In simple words, the async operation works, but React does not re-render the component the way you expect. In this article, we will explain why this happens and how to fix it using clear explanations and easy examples.
How React State Updates Work
In React, state updates are asynchronous and immutable. This means:
State updates do not happen immediately
React batches updates for performance
You should never modify the state directly
Example:
setCount(count + 1);
console.log(count); // Old value
Even after calling setCount, logging the value immediately shows the old state. This is normal React behavior.
Updating State After an Async API Call
A common pattern is fetching data and updating state.
Example:
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
If the UI does not update, the issue is usually not the API call, but how state is handled.
Mutating State Directly (Very Common Mistake)
React will not re-render if you modify state directly.
Wrong example:
users.push(newUser);
setUsers(users);
Correct example:
setUsers([...users, newUser]);
Always create a new copy of state when updating it.
Using Stale State in Async Code
Async code may capture old state values due to JavaScript closures.
Problem example:
setTimeout(() => {
setCount(count + 1);
}, 1000);
If count changes before the timeout runs, the update may be incorrect.
Correct example using functional update:
setTimeout(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
This ensures React always uses the latest state.
Missing Dependency in useEffect
If dependencies are missing, useEffect may not re-run when data changes.
Wrong example:
useEffect(() => {
fetchData();
}, []);
Correct example:
useEffect(() => {
fetchData(userId);
}, [userId]);
React needs correct dependencies to update state properly.
Setting State After Component Unmount
If a component unmounts before an async call finishes, state updates are ignored.
Example:
useEffect(() => {
let isMounted = true;
fetchData().then(data => {
if (isMounted) {
setData(data);
}
});
return () => {
isMounted = false;
};
}, []);
This avoids updating state on an unmounted component.
Not Handling Errors in Async Calls
If an API call fails silently, state may never update.
Example:
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
.catch(error => console.error(error));
Always handle errors so you know why state updates fail.
State Updates Are Batched
React batches multiple state updates for performance, which may look confusing.
Example:
setCount(count + 1);
setCount(count + 1);
This may only increase count by 1.
Correct approach:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
Comparison Table: Class State vs Hooks State Updates
The table below highlights how state updates differ between class components and hooks.

Join the conversation! Your thoughts help the community grow.