Day 17 – Handling Async useEffect, Loading and Error States in React
6 mins read

Day 17 – Handling Async useEffect, Loading and Error States in React

In the last blog, we learned how to fetch data with fetch and axios. But in real projects, things are never that simple.

When making an API call, we often face three main challenges:

  1. Loading State – We must show a spinner or “Loading…” text while waiting for the data.
  2. Error State – If something fails (e.g., server down, wrong URL), we must show an error message.
  3. Cleanup – If the user leaves the page before the API responds, React should not try to update state on an unmounted component.

This blog will teach you how to solve all three using Async useEffect in React.


Why Not Write async Directly in useEffect?

A common beginner mistake:

useEffect(async () => {
  const res = await fetch("https://api.example.com/data");
}, []);

❌ This is not correct.
React expects useEffect to either return nothing or a cleanup function. An async function returns a Promise instead, which confuses React.

✅ The correct way is:

useEffect(() => {
  const fetchData = async () => {
    // async code here
  };
  fetchData();
}, []);

Example 1 – Basic Async useEffect with Loading & Error

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

function UsersList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        let res = await fetch("https://jsonplaceholder.typicode.com/users");
        if (!res.ok) throw new Error("Failed to fetch users");

        let data = await res.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false); // stop loading in both success/fail
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}{u.email}</li>
      ))}
    </ul>
  );
}

export default UsersList;

Step-by-Step Explanation

  1. loading starts as true.
  2. While fetching → we show Loading users....
  3. If successful → data is stored in users state.
  4. If failed → error message is stored in error.
  5. finally ensures that loading becomes false no matter what.

👉 This pattern (loading → success/error → stop loading) is the standard data fetching cycle in React.


Example 2 – Cleaning up Async Requests

Imagine:

  • A user visits the page.
  • API call is still running.
  • User navigates away before response arrives.

Now React will try to call setState on an unmounted component, causing this warning:

Warning: Can't perform a React state update on an unmounted component.

Solution: Track Mounting State

useEffect(() => {
  let isMounted = true; // flag

  const fetchData = async () => {
    try {
      let res = await fetch("https://jsonplaceholder.typicode.com/posts");
      let data = await res.json();
      if (isMounted) setUsers(data); // only update if mounted
    } catch (err) {
      if (isMounted) setError(err.message);
    } finally {
      if (isMounted) setLoading(false);
    }
  };

  fetchData();

  return () => {
    isMounted = false; // cleanup on unmount
  };
}, []);

👉 Now even if the user leaves the page, React won’t try to update state after unmount.


Example 3 – Axios with Request Cancellation

Unlike fetch, Axios supports request cancellation natively.

import axios from "axios";
import { useEffect, useState } from "react";

function Products() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelToken = axios.CancelToken.source();

    const fetchProducts = async () => {
      try {
        let res = await axios.get("https://fakestoreapi.com/products", {
          cancelToken: cancelToken.token,
        });
        setProducts(res.data);
      } catch (err) {
        if (axios.isCancel(err)) {
          console.log("Request canceled:", err.message);
        } else {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();

    return () => cancelToken.cancel("Request canceled on unmount");
  }, []);

  if (loading) return <p>Loading products...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

👉 This ensures no wasted requests if the component unmounts.


Visual Flow

Initial RenderLoading = true
        |
        v
Fetch Data (async)
   | Success         | Failure
   v                 v
Update state        Set error
   |                   |
   v                   v
Loading = false   Loading = false

Best Practices

✅ Always handle loading and error states.
✅ Never use async directly in useEffect.
✅ Use cleanup (return () => {...}) to prevent memory leaks.
✅ For complex apps → prefer React Query or SWR.


External Links


Conclusion

Handling async useEffect in React is not just about fetching data — it’s about managing the whole lifecycle:

  • Show a loading indicator.
  • Handle errors gracefully.
  • Perform cleanup to avoid bugs.

👉 Next Blog (Day 18): React Router Basics – Routes, Links & Navigation

Leave a Reply

Your email address will not be published. Required fields are marked *