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:

Example:

JavaScript
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:

JavaScript
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:

JavaScript
users.push(newUser);
setUsers(users);

Correct example:

JavaScript
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:

JavaScript
setTimeout(() => {
  setCount(count + 1);
}, 1000);

If count changes before the timeout runs, the update may be incorrect.

Correct example using functional update:

JavaScript
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:

JavaScript
useEffect(() => {
  fetchData();
}, []);

Correct example:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
setCount(count + 1);
setCount(count + 1);

This may only increase count by 1.

Correct approach:

JavaScript
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.

AspectClass ComponentsFunction Components (Hooks)
State update methodthis.setState()useState() setter
Async behaviorBatchedBatched
Access to latest stateCallback in setStateFunctional updates
Common issuethis bindingStale closures
Recommended todayNoYes

Real-World Examples with Axios and async/await

Axios Example

JavaScript
import axios from 'axios';

function Users() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    async function loadUsers() {
      try {
        const response = await axios.get('/api/users');
        setUsers(response.data);
      } catch (error) {
        console.error('Failed to load users', error);
      }
    }
    loadUsers();
  }, []);

  return <div>{users.length} users loaded</div>;
}

This pattern ensures errors are handled and state updates correctly.

Debugging Checklist for React State Issues

When state does not update as expected, check the following:

Following this checklist helps quickly identify issues.

React 18 Batching and Concurrency Behavior

React 18 introduced automatic batching for more scenarios, including async operations.

Example:

JavaScript
setCount(c => c + 1);
setFlag(f => !f);

React may batch these updates into a single render.

Key points:

Understanding batching helps avoid false assumptions while debugging.

Best Practices to Fix Async State Issues

To avoid state update problems:

These practices solve most React async state issues.

Summary

React components may not update state correctly after an asynchronous call due to direct state mutation, stale closures, missing dependencies, or incorrect update patterns. By understanding how React state works, using functional updates, handling async calls properly, and following best practices, you can ensure your components re-render reliably and display the correct data.