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:
- Why the function passed to
useEffectcannot beasync. - How to define and call an asynchronous function inside an Effect.
- How
await axios.get()works. - How to save API data with
useState. - How
[count]causes an Effect to run again. - How to display API data with safe TypeScript types.
- 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
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
axios.get(url)sends a GET request and gives us a Promise.awaitpauses the current async function until the Promise settles. It does not block the entire React application.responsecontains Axios's response object;response.datacontains 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
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
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 whencountchanges.
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
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
- Can
useEffect(async function () { ... })be used? No: it returns a Promise. - Does declaring
fetchDataexecute it? No: callfetchData(). - Does
awaitfreeze all of React? No: it pauses only the async function. - What does
setPerson(response.data)do? Updates state and triggers a render. - What does
[count]do? Re-runs the Effect whencountchanges. - If
countbecomes2, which path is requested?/api/people/2. - What does
Person | nullmean? Either aPersonobject ornull. - Where does a failed Axios request go? To
catch. - What does
setError("Network error!")do? Updates the UI's error message. - When does
finallyrun? After success or failure. - Why reset errors before a request? To remove the previous error.
- Why not put
setIsLoading(false)only at the end oftry? It would be skipped after a failure.
Summary
- Put an async function inside
useEffect, then call it. - Use
awaitinside that async function. - Use
[count]when the request depends oncount. - Type API data, for example
axios.get<Person>()anduseState<Person | null>(null). - Use
try...catchfor failures andfinallyto finish loading. - Clear stale errors and consider stale requests when dependencies change.