iT邦幫忙

2025 iThome 鐵人賽

DAY 15
1

為什麼框架會影響你怎麼用 signals?

簡單回答就是 生命週期,但在脆上有批 React 仔永遠當它不存在的在賣課,確實也蠻擔心的,但你深入到一定的程度,就會發現那是一個賣課的話術。

經歷我們前面的實作,你應該也能深刻體會到生命週期的意思,這篇就來代你釐清三個重點:

  • 兩階段心智:React 有 Render 和 Commit,Render 可多次、可打斷;Commit 才去動 DOM。
  • 時機對齊:我們的 signals 用 microtask 合併副作用(scheduler),React 有 事件批次Concurrent 渲染。
  • 禁區與做法:什麼不能放在 React 的 Render 時機(例如createEffectsignal.set),正確放在哪裡。

TL;DR

  • 在 React 裡,render 必須純(Function Component 的概念):不要在 render 期做副作用或寫入 signals。
  • 副作用分工:
    • UI 相關(DOM 量測、動畫)→ useLayoutEffect / useEffect
    • 資料流相關(依信號值觸發業務、請求)→ signals 的 createEffect(透過適配器管理生命週期)
  • 訂閱一律走 useSyncExternalStore(下一篇會寫),避免 tearing。

React 與 signals 的各自時機

https://ithelp.ithome.com.tw/upload/images/20250818/201290207yDRIt2ijY.png

  • Render 可能跑很多次(Concurrent、StrictMode),Commit 只有一次。
  • 我們的 scheduler 在 microtask 時間點合併重跑 effect
  • 因此千萬別在 render 期創建或觸發你的 createEffect;交給 hook 幫你管。

StrictMode 與 Concurrent 陷阱

  • StrictMode(dev mode):React 會「建立一次 → 立刻清理 → 再建立一次」,用來抓你 render 期的副作用。
  • Concurrent:Render 可以被打斷與重做,所以 render 期讀外部可變狀態會導致 撕裂 (tearing)。
    • 官方建議: 用 useSyncExternalStore快照 + 訂閱,React 能在 commit 前重取快照,防 tearing。

不能這樣做

  • 在 render 期(Function Component)裡直接使用 createEffect
function Bad() {
  // render 期創建外部 effect,破壞純度與可預測性
  createEffect(() => {
    console.log("value", someSignal.get());
  });
  return <>{/**...你的UI */}</>;
}
  • 在 render 期(Function Component)直接使用 signal.set
function Bad() {
  const v = someSignal.get();
  if (v < 0) someSignal.set(0); // render 期間寫入,會導致無窮重渲染
  return <>{/**...你的UI */}</>;
}
  • 直接把 get() 值塞進閉包長期使用
function Bad() {
  const v = someSignal.get();  // 這是當下快照
  const onClick = () => console.log(v); // 之後永遠印舊值
  return <button onClick={onClick}>log</button>;
}

正確做法

  • 訂閱:用 useSyncExternalStore 包一個 useSignalValue(src)
    • snapshot 使用 peek()(不建立 React→signal 依賴,但在 stale 時會 lazy 重算)。
    • subscribe 內部用 createEffect 監聽來源變動,並在清理時解除。
  • 寫入:放在事件處理或 React 的 effect 內(不是 render 期)。
  • DOM 副作用:依然走 useLayoutEffect / useEffect;把 signals 的值傳給 React,由 React 掌控 DOM 時機。

這邊先不實作,給個概念讓大家先思考。

React 的批次 vs 我們的 batch

React 在事件處理內會自動批次 setState;我們的 batch 只影響 signals 的 effect 合併重跑,兩者互不衝突。
https://ithelp.ithome.com.tw/upload/images/20250818/20129020KQkMHWtTSj.png

範例:

batch(() => {
    a.set(10)
    b.set(20)
    a.set(30)
});
// our effect 只有在 microtask 重跑一次
// React 如果有 setState 也會在事件內批次 render 一次

副作用分工表

事情 放哪裡 說明
讀寫 DOM、量測、動畫 useLayoutEffect / useEffect 由 React 的 Commit 管理時機
根據信號值觸發業務副作用 createEffect(透過 hook 訂閱) 我們的 scheduler 會合併重跑
多次更新只想重跑一次 batchtransaction 影響 signals 的 effect,不影響 React 的 commit
讓 React 訂閱外部狀態 useSyncExternalStore 避免 tearing,支援 Concurrent
測試或示範想立刻重跑 our effects flushSync()(我們的) 立刻清空 scheduler 佇列

錯誤 vs 正確對照

錯誤:render 期(Function Component)建立訂閱 State + Effect

function Bad() {
  const [state, setState] = useState();
  const v = someSignal.get();
  useEffect(() => {
    // 這樣訂閱常會 tearing 或重複更新
    const stop = createEffect(() => {
      someSignal.get();
      setState(Date.now());
    });
    return () => stop();
  }, []);
  return <div>{v}</div>; // v 不是 React 管控的快照,要使用 state 做封包
}

正確:用 useSyncExternalStore 取得快照

function Good() {
  // 下一篇會實作這個hook
  const v = useSignalValue(someSignal);
  return <div>{v}</div>; // v 是 React 管控的同步快照,透過custom hook的方式整理
}

結語

有了上述的概念,你應該對 React 的狀態管理和更新機制,有了比較正確的認知。

我們的目的不是要取代 React 的狀態,而是教你如何把 signals 當外部資料源,在 Concurrent 模式下不撕裂、少重跑、行為可預期。

下一篇,我們來實作能在 React 環境下正確使用前面建立的 signal 系統。


上一篇
實作 Batch & Transaction
下一篇
React 應用(II):不撕裂的訂閱
系列文
Reactivity 小技巧大變革:掌握 Signals 就這麼簡單!17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言