← React Course

REACT CORE · 99

Effects / Data Fetching / Cleanup:Effect 是同步外部系統,不是萬用初始化區

如果一個值能由 props/state 直接計算,就先在 render 算。Effect 主要用來同步 React 外部世界,例如 network、timer、DOM API、subscription。

Learning outcomes

1. Derived data 不需要另一份 state

const passed =
  courses.filter(
    course =>
      course.score !== null
      && course.score >= 60
  );

不要:

const [
  passed,
  setPassed
] = useState([]);

useEffect(() => {
  setPassed(
    courses.filter(...)
  );
}, [courses]);

後者多了一份必須同步的 state,增加 inconsistency 風險。

2. Effect:同步 React 外部系統

useEffect(() => {
  document.title =
    "Courses: "
    + courses.length;
}, [courses.length]);

document.title 是 React state 以外的 Browser system,因此 effect 合理。

3. Fetch + loading/error state

useEffect(() => {
  const controller =
    new AbortController();

  async function load() {
    setStatus("loading");

    try {
      const response =
        await fetch(
          "/api/courses",
          {
            signal:
              controller.signal
          }
        );

      if (!response.ok) {
        throw new Error(
          "HTTP "
          + response.status
        );
      }

      const data =
        await response.json();

      setCourses(data);
      setStatus("success");
    } catch (error) {
      if (
        error.name
        === "AbortError"
      ) {
        return;
      }

      setStatus("error");
    }
  }

  load();

  return () => {
    controller.abort();
  };
}, []);

4. Cleanup 的目的不是儀式

component unmount、dependency 改變前,上一個 effect 需要停止 subscription / timer / request,否則舊工作可能繼續影響已經不再需要的 UI。

5. Stale response race

使用者快速切換 course id:

id = 1 → request A
id = 2 → request B

B 先回來
A 後回來

若不取消/guard A,舊資料可能覆蓋新畫面。這和 132 Advanced Async 是同一個核心問題,只是現在發生在 React lifecycle。

6. Dependencies:不是「想跑幾次就怎麼填」

Dependency array 應反映 effect 內使用的 reactive values,而不是拿來手動控制 lifecycle。若 effect 依賴某個 prop/state,就應思考它變更時是否需要重新同步。

Project checkpoint:Course Dashboard v4

mountfetch /api/coursesloadingvalidate/parsesetCoursesrender

到這裡 Course Dashboard 已經從 local-only app 變成 client UI。

Debug evidence:infinite effect loop

useEffect(() => {
  setCourses(
    [...courses]
  );
}, [courses]);

effect 改 courses → courses change → effect 再跑。先畫 dependency / state transition loop,不要只亂刪 dependency。

Knowledge check

  1. 為什麼 derived data 通常不需要 useEffect?
  2. cleanup 解決哪種 lifecycle / stale work 問題?
  3. 設計一個 id 改變時會取消舊 fetch 的 effect。
  4. 看到 effect 無限重跑時,如何畫出 state transition loop?