React

Lesson 07

Async effect cleanup

Guard async effects so stale responses cannot update the current screen.

Good Code

ReviewDetails.tsx
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;
}

Bad Code

ReviewDetails.tsx
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>;
}

Review Notes

What to review

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.

Bad Code

The bad version starts a new request for every reviewId but allows older requests to finish later and overwrite the latest review.

Takeaways

  • Clean up async effects when inputs change or the component unmounts.