← Back

React.js + Next.js · Module 3. Forms and Effects · Lesson 10 · Point 10

Async Axios Requests in useEffect

Level: Beginner · Stack: React, TypeScript, Axios · File: App.tsx

In this lesson, you will learn:

  1. Why the function passed to useEffect cannot be async.
  2. How to define and call an asynchronous function inside an Effect.
  3. How await axios.get() works.
  4. How to save API data with useState.
  5. How [count] causes an Effect to run again.
  6. How to display API data with safe TypeScript types.
  7. How to handle errors, loading, and finally.

Reminder: An Effect runs after React commits the render. In development, React Strict Mode can run an Effect an additional time to help detect bugs.


REACT · 01

1. Do Not Make the Effect Function Async

Incorrect:

useEffect(async function () {
  const response = await axios.get("https://swapi.info/api/people/1");
  console.log(response.data);
}, []);

Every async function returns a Promise. React expects the function passed directly to useEffect to return either nothing (undefined) or a cleanup function, not a Promise.

Remember: Do not put async on the function passed directly to useEffect.


REACT · 02

2. Declare and Call an Async Function Inside the Effect

import { useEffect } from "react";

export default function App() {
  useEffect(function () {
    // Declare the asynchronous function.
    async function fetchData() {
      console.log("Fetching data...");
    }

    // A declaration alone does not execute the function.
    fetchData();
  }, []);

  return <h1>Hello React!</h1>;
}

Browser Result

Hello React!

Console: Fetching data...

The component renders its heading, and the called async function writes to the console after the render is committed.
  • useEffect(function () { ... }, []) registers an Effect.
  • async function fetchData() declares an asynchronous function.
  • fetchData() calls it. Without this line, the log does not happen.

Check yourself: If you remove fetchData(), will the function's body run? No.


REACT · 03

3. Wait for an Axios Request

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

export default function App() {
  useEffect(function () {
    async function fetchCharacter() {
      // Wait for this request to finish (without freezing the app).
      const response = await axios.get(
        "https://swapi.info/api/people/1"
      );

      // The API's response body is in response.data.
      console.log(response.data);
    }

    fetchCharacter();
  }, []);

  return <h1>Star Wars API</h1>;
}

Browser Result

Star Wars API

The returned character object appears in the browser console.

The page stays responsive while Axios waits for the Star Wars API response.
  • axios.get(url) sends a GET request and gives us a Promise.
  • await pauses the current async function until the Promise settles. It does not block the entire React application.
  • response contains Axios's response object; response.data contains the returned data.

REACT · 04

4. Save Typed API Data in State

To show fetched data in the UI, store it in state. In TypeScript, tell React which data type you expect.

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

type Person = {
  name: string;
};

export default function App() {
  // Initially, there is no person.
  const [person, setPerson] = useState<Person | null>(null);

  useEffect(function () {
    async function fetchCharacter() {
      const response = await axios.get<Person>(
        "https://swapi.info/api/people/1"
      );

      // Update state with API data.
      setPerson(response.data);
    }

    fetchCharacter();
  }, []);

  return <pre>{JSON.stringify(person, null, 2)}</pre>;
}

Browser Result

{
  "name": "Luke Skywalker"
}
After the request succeeds, React renders the typed API object as formatted JSON.

Person | null means the state can hold a Person object or null. This is important: useState(null) alone would infer only null in TypeScript.

setPerson(...) tells React to update state and render again. JSON.stringify(person, null, 2) turns the object into indented text, and <pre> preserves its formatting.


REACT · 05

5. Run the Effect Again When count Changes

import { useEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(1);

  useEffect(function () {
    console.log("Fetching character:", count);
  }, [count]); // Run after mount and when count changes.

  function handleNextCharacter() {
    setCount(count + 1);
  }

  return (
    <div>
      <h2>Character number: {count}</h2>
      <button onClick={handleNextCharacter}>Next character</button>
    </div>
  );
}

Browser Result

Character number: 2

After one click, count becomes 2, React renders again, and the Effect runs with the new value.

On the initial mount, the Effect sees count = 1. After a click changes it to 2, React renders again and the Effect runs with 2.

  • []: no changing dependencies; the Effect does not re-run because state changes.
  • [count]: the Effect runs again when count changes.

REACT · 06

6. Put count in the Request URL

useEffect(function () {
  async function fetchCharacter() {
    const response = await axios.get(
      `https://swapi.info/api/people/${count}`
    );

    console.log(response.data);
  }

  fetchCharacter();
}, [count]);

The backticks create a template literal. ${count} inserts the current value.

If count is 1, Axios requests:

https://swapi.info/api/people/1

If count becomes 2, Axios requests:

https://swapi.info/api/people/2

REACT · 07

7. Display Data Safely with TypeScript

type Person = {
  name: string;
};

const [person, setPerson] = useState<Person | null>(null);

// After the request succeeds:
setPerson(response.data);

// In JSX:
// Show the name if person exists; otherwise show Loading...
<p>Name: {person ? person.name : "Loading..."}</p>

The expression person ? person.name : "Loading..." is a ternary condition. It avoids reading .name from null.

This is a teaching snippet showing the related lines, not a complete component. Note that person === null is not a perfect loading indicator: it can also mean that a request failed. A dedicated isLoading state is better.


REACT · 08

8. Catch Failed Axios Requests

async function fetchCharacter() {
  try {
    // If the request succeeds, update state.
    const response = await axios.get<Person>(
      "https://swapi.info/api/people/1"
    );
    setPerson(response.data);
  } catch (error) {
    // If the request fails, handle the error.
    console.log("Error fetching character:", error);
  }
}

try contains work that might fail. If Axios rejects the Promise, JavaScript skips the remaining code in try and moves to catch. A successful request does not run catch.


REACT · 09

9. Show an Error Message in the UI

// At the top of the component:
const [error, setError] = useState("");

// Inside the async function:
try {
  await axios.get("https://swapi.info/api/people/1");
} catch {
  setError("Failed to load character!");
}

// In JSX:
{error && <p>{error}</p>}

The empty string means no message. After setError("Failed to load character!"), React renders again; error && ... displays the paragraph. A console message alone would not tell the user what happened.


REACT · 10

10. Track Loading and Always Finish It

const [isLoading, setIsLoading] = useState(false);

async function fetchCharacter() {
  setIsLoading(true); // Request starts.

  try {
    await axios.get("https://swapi.info/api/people/1");
  } catch (error) {
    console.log("Error:", error);
  } finally {
    setIsLoading(false); // Request finished, successful or not.
  }
}

// In JSX:
{isLoading && <p>Loading...</p>}

true shows “Loading...”; false hides it. finally executes after success or failure in this ordinary request flow.

Use loading and error state together:

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");

async function fetchCharacter() {
  setIsLoading(true); // Start showing loading.
  setError("");       // Clear an old error.

  try {
    await axios.get("https://swapi.info/api/people/1");
  } catch {
    setError("Failed to load character!");
  } finally {
    setIsLoading(false); // Stop showing loading.
  }
}

Why call setError("") before a new request? Suppose an earlier request failed, but the user tries again. The old message should disappear while we try the new request.


REACT · 11

11. Put setIsLoading(false) in finally

Incorrect:

try {
  await axios.get("/api/people/1");
  setIsLoading(false); // Skipped if axios.get throws.
} catch {
  setError("Something went wrong!");
}

If the request fails, setIsLoading(false) is skipped. The page may remain stuck on “Loading...”.

Correct:

try {
  await axios.get("/api/people/1");
} catch {
  setError("Something went wrong!");
} finally {
  setIsLoading(false); // Runs after success or failure.
}

REACT · 12

12. Put Everything Together

This version puts the pieces together: typed data, button, Effect dependency, loading, errors, and a name on the page.

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

type Person = {
  name: string;
};

export default function App() {
  const [count, setCount] = useState(1);
  const [person, setPerson] = useState<Person | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(function () {
    // Ignore results from an outdated request after cleanup.
    let ignore = false;

    async function fetchCharacter() {
      setIsLoading(true);
      setError("");
      setPerson(null);

      try {
        // Fetch the character matching the current count.
        const response = await axios.get<Person>(
          `https://swapi.info/api/people/${count}`
        );

        if (!ignore) {
          setPerson(response.data);
        }
      } catch {
        if (!ignore) {
          setError("Failed to load character!");
        }
      } finally {
        if (!ignore) {
          setIsLoading(false);
        }
      }
    }

    fetchCharacter();

    // React calls this when the Effect is replaced or removed.
    return function cleanup() {
      ignore = true;
    };
  }, [count]);

  function handleNextCharacter() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Star Wars Characters</h1>
      <h2>Character number: {count}</h2>

      <button onClick={handleNextCharacter}>
        Get next character
      </button>

      {isLoading && <p>Loading...</p>}
      {error && <p role="alert">{error}</p>}
      {!isLoading && person && <p>Name: {person.name}</p>}
    </div>
  );
}

Browser Result

Star Wars Characters

Character number: 1

Name: Luke Skywalker

The completed component requests the selected character and displays loading, error, or success state.

One small improvement beyond the original lesson: The ignore flag and cleanup function prevent an older request from overwriting a newer character if the user clicks quickly. It does not cancel the network request; it just ignores its outdated result. We can study cleanup in more detail later.

Practical note: The examples assume Axios is installed (npm install axios). API requests can fail because of connectivity or server issues; this is why error handling matters.

Check Yourself

  1. Can useEffect(async function () { ... }) be used? No: it returns a Promise.
  2. Does declaring fetchData execute it? No: call fetchData().
  3. Does await freeze all of React? No: it pauses only the async function.
  4. What does setPerson(response.data) do? Updates state and triggers a render.
  5. What does [count] do? Re-runs the Effect when count changes.
  6. If count becomes 2, which path is requested? /api/people/2.
  7. What does Person | null mean? Either a Person object or null.
  8. Where does a failed Axios request go? To catch.
  9. What does setError("Network error!") do? Updates the UI's error message.
  10. When does finally run? After success or failure.
  11. Why reset errors before a request? To remove the previous error.
  12. Why not put setIsLoading(false) only at the end of try? It would be skipped after a failure.

Summary

  • Put an async function inside useEffect, then call it.
  • Use await inside that async function.
  • Use [count] when the request depends on count.
  • Type API data, for example axios.get<Person>() and useState<Person | null>(null).
  • Use try...catch for failures and finally to finish loading.
  • Clear stale errors and consider stale requests when dependencies change.

← Back↑ Back to the top