跳至主要内容
useSignal() is the Future of Web Frameworks

from MIŠKO HEVERY

Intermediate

/

React / Next

useSignal() 常見於 React 以外的框架,例如 Vue、Preact、Solid 和 Qwik。這篇文章以 React 為基礎,比較了 useState()useSignal() 的差異,並解釋了為什麼 useSignal() 是未來的趨勢。

useSignal() 是一種類似於 React 中的 useState() 儲存應用程序狀態的方式。不同之處在於,useSignal() 返回一個 getter 和一個 setter,而 React 的 useState() 則返回一個 value 和一個 setter。這讓 Signal 可以透過訂閱者模式來追蹤誰在使用 state,並在 state 更新時通知訂閱者,減少不必要的 re-render。

useState() vs useSignal()

兩者最根本的差異是 useState() 回傳的第一個值是 value(StateValue),而 useSignal() 回傳的第一個值是 getter(StateReference)。回傳 getter 的好處是,它可以在需要時才取得 value,這樣可以減少不必要的計算。這也是為什麼 useSignal() 可以在不觸發 re-render 的情況下更新 state 的原因。以下是一個 SolidJS 的例子:

SolidJS Counter with useSignal()
export function Counter() {
const [getCount, setCount] = createSignal(0);
return <button onClick={() => setCount(getCount() + 1)}>{getCount()}</button>;
}

在 Signal 系統中,呼叫 getter (getCount()) 的人,等於訂閱了該 Signal,而 setter (setCount()) 則是通知所有訂閱者更新 state,這代表著 Signal 系統可以知道誰在使用 state,並在 state 更新時通知訂閱者,是一個 reactive 的系統。

在 React 中,useState() 回傳的第一個值是 value,這代表著 React 無法知道 state 被使用的情況,因此每次 state 更新時,都會重新 render 整個 component tree,是一個非 reactive 的系統,而且計算量也會比較大。

useRef()

React 的 useRef()useSignal() 十分相似,因為 useRef() 也是 reference 的概念,只是它缺乏了訂閱追蹤和通知的功能。而在 Signal 系統中,useSignal() 也可以作為 useRef() 來使用。

useMemo()

考慮以下 React useMemo 的例子:即使我們把 <Display/> 包在 React.memo(),但是當 state 更新時,<Counter/> 和裝著 countA 的 <Display/> 都會重新 render。

即時編輯器
結果
Loading...
Console output
# Initial render output
<Counter />
<Display count={0}/>
<Display count={0}/>

# "A" button clicked
<Counter />
<Display count={1}/>

但 Signal 系統完全不需要 useMemo(),因為它本身就能找到需要 state 的地方,並在 state 更新時通知那些訂閱者,因此不需要額外的 memoization。考慮以下 Qwik 的例子:

Qwik Counter with useSignal()
export function Counter() {
console.log(`<Counter />`);
const countA = useSignal(0);
const countB = useSignal(0);
return (
<div>
<button onClick$={() => countA.value++}>A</button>
<button onClick$={() => countB.value++}>B</button>
<Display count={countA.value} />
<Display count={countA.value} />
</div>
);
}

export const Display = component$(({ count }: { count: number }) => {
console.log(`<Display count={${count}}/>`);
return <div>{count}</div>;
});
Console output
# Initial render output
<Counter />
<Display count={0}/>
<Display count={0}/>

# "A" button clicked
# nothing is re-rendered

可以看到,當 state 更新時,除了不會重新 render <Counter/>,也不會重新 render <Display/>,因為 Signal 系統已經知道使用 count 的地方只有 <Display/> 中的 text node,因此只會更新 text node。

Signal 系統並不是一個新的概念,它在 knockout.js 甚至更早的框架前就已經出現過。只是近幾年的 compiler 和 JSX 發展,讓 Signal 系統變得更加簡潔且容易使用。