Good Code
The good version marks the request as cancelled during cleanup, so an older response cannot update state after the component has moved on.
Lesson 07
Guard async effects so stale responses cannot update the current screen.
import { useEffect, useState } from "react";
type Review = {
id: string;
title: string;
status: string;
};
async function fetchReview(reviewId: string): Promise<Review> {
const response = await fetch("/api/reviews/" + reviewId);
if (!response.ok) {
throw new Error("Could not load review.");
}
return response.json();
}
export function ReviewDetails({ reviewId }: { reviewId: string }) {
const [review, setReview] = useState<Review | null>(null);
const [status, setStatus] = useState<"idle" | "loading" | "ready" | "error">(
"idle",
);
useEffect(() => {
// Cleanup prevents stale responses from updating the current screen.
let cancelled = false;
setStatus("loading");
fetchReview(reviewId)
.then((nextReview) => {
if (!cancelled) {
setReview(nextReview);
setStatus("ready");
}
})
.catch(() => {
if (!cancelled) {
setStatus("error");
}
});
return () => {
cancelled = true;
};
}, [reviewId]);
if (status === "loading") {
return <p>Loading review...</p>;
}
if (status === "error") {
return <p role="alert">Could not load review.</p>;
}
return review ? <h2>{review.title}</h2> : null;
}import { useEffect, useState } from "react";
type Review = {
id: string;
title: string;
status: string;
};
async function fetchReview(reviewId: string): Promise<Review> {
const response = await fetch("/api/reviews/" + reviewId);
return response.json();
}
export function ReviewDetails({ reviewId }: { reviewId: string }) {
const [review, setReview] = useState<Review | null>(null);
useEffect(() => {
// Older requests can still win the race and overwrite state.
fetchReview(reviewId).then(setReview);
}, [reviewId]);
return review ? <h2>{review.title}</h2> : <p>Loading review...</p>;
}The good version marks the request as cancelled during cleanup, so an older response cannot update state after the component has moved on.
The bad version starts a new request for every reviewId but allows older requests to finish later and overwrite the latest review.