Load remote data with fetch inside an effect, cleanly modeling the loading and error states.
Open this lesson in KodokonA network request takes time and can fail: your interface must reflect those realities. The professional convention is to track three distinct pieces of state: the data received, a loading boolean, and a possible error. The request goes inside a useEffect, because contacting a server is a side effect.
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://api.example.com/users")
.then((res) => {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then((data) => setUsers(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);In the render, handle the cases in order: loading first, then the error, and finally the data. These early returns keep the code readable and guarantee that the user always sees something coherent: a waiting indicator, a failure message, or the expected list.
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);One last trap: if a dependency changes (a userId, for example), a new request goes out while the old one may still be in flight. If the old one answers last, it would overwrite the fresh data. The classic countermeasure: an ignore flag set to true by the cleanup function, to throw away stale responses.
useEffect(() => {
let ignore = false;
setLoading(true);
fetch("/api/users/" + userId)
.then((res) => res.json())
.then((data) => {
if (!ignore) setUser(data);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [userId]);catch never fires. Why?ignore flag, set to true in the effect's cleanup, used for?