Good Code
The good version lists reviewId as a dependency and aborts in-flight work when the component unmounts or the review changes.
Lesson 06
List every effect dependency and clean up async work when inputs change.
import { useEffect, useState } from "react";
type Comment = {
id: string;
body: string;
};
export function ReviewComments({ reviewId }: { reviewId: string }) {
const [comments, setComments] = useState<Comment[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// The effect follows reviewId and cancels stale network work.
const controller = new AbortController();
async function loadComments() {
setError(null);
const response = await fetch("/api/reviews/" + reviewId + "/comments", {
signal: controller.signal,
});
if (!response.ok) {
setError("Could not load comments.");
return;
}
setComments(await response.json());
}
loadComments().catch((error) => {
if (error.name !== "AbortError") {
setError("Could not load comments.");
}
});
return () => controller.abort();
}, [reviewId]);
if (error) {
return <p role="alert">{error}</p>;
}
return comments.map((comment) => <p key={comment.id}>{comment.body}</p>);
}import { useEffect, useState } from "react";
type Comment = {
id: string;
body: string;
};
export function ReviewComments({ reviewId }: { reviewId: string }) {
const [comments, setComments] = useState<Comment[]>([]);
useEffect(() => {
// Empty dependencies freeze the first reviewId this render saw.
fetch("/api/reviews/" + reviewId + "/comments")
.then((response) => response.json())
.then(setComments);
}, []);
return comments.map((comment) => <p key={comment.id}>{comment.body}</p>);
}The good version lists reviewId as a dependency and aborts in-flight work when the component unmounts or the review changes.
The bad version captures the initial reviewId forever, so it can display stale comments when the prop changes.