iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0

快速回顧

這篇接續上一篇的結尾,讓我們來透過實際的範例,理解要怎麼互補使用。

Effect 清理策略

首先,我們先來釐清主要的概念:

  • 我們的 effect(createEffect)負責資料/業務的循環工作,依賴 signal,重跑前以 onCleanup 釋放資源。
  • React effect(useEffect / useLayoutEffect)負責UI/DOM的循環工作,依賴 React 的快照/props/state,在 下一次 commit 前清理。

實際範例

計時器範例(用 createEffect

  • 需求:根據可調整的 intervalMs 輪詢資料,並把最新時間寫進 heartbeat signal。
  • 重點:當 intervalMs 改變時,自動清掉舊的 setInterval 並重建新的。
// data/heartbeat.ts
import { signal } from "../core/signal";
import { createEffect, onCleanup } from "../core/effect";

export const intervalMs = signal(1000);
export const heartbeat = signal<Date | null>(null);

createEffect(() => {
  const ms = intervalMs.get(); // 依賴 signal
  const id = setInterval(() => {
    heartbeat.set(new Date()); // 寫回資料層
  }, ms);

  onCleanup(() => clearInterval(id)); // 下一次重跑前釋放
});

  • 為什麼放在 createEffect
    這是純「資料層」工作:依賴 signal 並回寫 signal;不和 DOM 綁定、也不需 React 的 commit 時機。

UI 閃爍(用 useEffect

  • 需求:讓游標每 500ms 閃爍一次。
  • 重點:與 DOM/渲染緊密相關,應交給 React 的生命週期管理。
// ui/Blinker.tsx
import { useState, useEffect } from "react";

export function Blinker({ enabled = true }) {
  const [on, setOn] = useState(false);

  useEffect(() => {
    if (!enabled) return;
    const id = setInterval(() => setOn(v => !v), 500);
    return () => clearInterval(id); // 下一次 commit 前清理
  }, [enabled]);

  return <span className={on ? "caret on" : "caret"}>|</span>;
}

  • 為什麼放在 useEffect
    這是純「UI/視覺」行為:依賴的是 React 的 state/props,清理點應跟隨 React 的 commit 週期。

交互使用、互不干擾

// App.tsx
import { useSignalValue } from "./react-adapter";
import { heartbeat, intervalMs } from "../data/heartbeat";
import { Blinker } from "./ui/Blinker";

export function Dashboard() {
  const lastBeat = useSignalValue(heartbeat);
  const ms = useSignalValue(intervalMs);

  return (
    <section>
      <h3>Last heartbeat: {lastBeat?.toLocaleTimeString() ?? "—"}</h3>
      <p>Polling every {ms} ms</p>
      <Blinker enabled /> {/* UI 計時由 React 管 */}
    </section>
  );
}

  • 計時器輪詢(createEffect)獨立於任何元件存在,跨頁仍持續。
  • UI 閃爍(React useEffect)隨元件掛載/卸載而建立/清除。
  • 清理時機互不衝突:createEffectonCleanup 在重跑前執行;React useEffect 的 cleanup 在下一次 commit 前執行。

錯誤與更正

錯誤:用 createEffect 控制 DOM 處理計時器

createEffect(() => {
  const id = setInterval(() => {
    el.classList.toggle("on"); // DOM 時機不受 React 管
  }, 500);
  onCleanup(() => clearInterval(id));
});

更正:交給 React useEffect
(前面 Blinker 範例)

錯誤:用 React useEffect 做資料輪詢且用 signal 直接當依賴

// 只 mount 一次,不會因 interval 改變而重建
React.useEffect(() => {
  const id = setInterval(fetchData,  ms /* 直接丟 signal */);
  return () => clearInterval(id);
}, []); // ← deps 空陣列

更正:兩種情況,擇一即可

// A) 使用前面封裝好的 hook,取得快照使用
const ms = useSignalValue(intervalMs);
React.useEffect(() => {
  const id = setInterval(fetchData, ms);
  return () => clearInterval(id);
}, [ms]);

// B) 回歸資料層:用 createEffect(不要在 React Component 裡面使用 signal 提供的 function 「讓他在 component 閉包之外」,讓一切回歸單純,要應用 signal 時,再透過 hook 處裡使用)

快速整理觀念

情境 建議位置 建立依賴 清理時機
資料輪詢、快取更新、寫入 signal our effect signal.get() 重跑前 onCleanup
視覺效果、DOM 操作、量測 React effect React 的 state/props 下一次 commit 前 cleanup
同步多次 set() 合併 scheduler(內建 microtask) microtask flush
讀取當前值、避免反向依賴 useSignalValue / peek() 不追蹤

透過心智圖理解

  • 各自的職責
    https://ithelp.ithome.com.tw/upload/images/20250821/20129020IS40BgFTLU.png

  • 清理順序(一次更新)
    https://ithelp.ithome.com.tw/upload/images/20250821/20129020pFljNdNE7N.png

常見問題

  • Q: 為什麼 getSnapshotpeek() 而不是 get()
    A: 避免 React 元件本身被加入 reactive graph 的 subspeek() 仍會在 stale 時完成 lazy 重算,快照不會舊。

  • Q: 我能在 render 期(function component內)使用 createEffect 嗎?
    A: 盡量不要。最好還是由 adapter 的 subscribeReadableuseSyncExternalStore 的生命週期內建立與清理。

  • Q: 多次同步 set() 要手動 batch 嗎?
    A: 不用。我們的 scheduler 會把同一輪更新合併到一個 microtask。跨 await 的合併將在進階篇(transaction(async))再談。

結語

有了上面的範例之後,應該能理解結合使用 signal 的好處,用起來就和 jotai / zustand 的感覺差不多,實際上要更加精細,而且不會跟特定框架綁定,相容性更高。

下一篇,我們來針對 keys 重掛、stale closure、Transition 下的讀取值一致性,給出具體「可以直接抄」的模式與檢查表。


上一篇
React 應用(III):生命週期沒消失—副作用分工與清理時機
下一篇
React 應用(V):高頻陷阱與最佳實務(I)
系列文
Reactivity 小技巧大變革:掌握 Signals 就這麼簡單!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言