Why is it so complex?
While we wait for an async request, the component props can change or the component can unmount. An older request can also finish last and overwrite a newer result. 🐛
How to fix
Cancel the request during cleanup and ignore results from the old effect. With Axios, use AbortController.
This example expects your own /api/joke endpoint to return { "joke": "..." }.
import React, { useState, useEffect } from "react";
import axios from "axios";
export default function RandomJoke({ more, loadMore }) {
const [joke, setJoke] = useState("");
useEffect(() => {
const controller = new AbortController();
let active = true;
async function fetchJoke() {
try {
const response = await axios("/api/joke", {
signal: controller.signal,
});
if (active) setJoke(response.data.joke);
} catch (err) {
if (active && !axios.isCancel(err)) console.error(err);
}
}
fetchJoke();
return () => {
active = false;
controller.abort();
};
}, [more]);
return (
<div>
<h1>Here's a random joke for you</h1>
<h2>{joke}</h2>
<button onClick={loadMore}>More...</button>
</div>
);
}
Each effect run gets its own controller and active flag. Cleanup aborts the request and prevents stale results from updating state. This also handles React Strict Mode's extra setup/cleanup cycle. ✅
Conclusions
Keep cleanup close to the async work. Next, let's move it into a reusable hook.
