restate / v1.0.0
只訂閱需要的部分
選擇每個元件依賴的 store 部分,以 shallow 比較物件,以 batch 合併變化,並串接外部事件。
傳入 selector
呼叫 Hook 時傳入一個函式,挑出元件需要的內容。每次變化後,restate 會執行 selector,並用 Object.is 與上一次的結果比較。若相等,元件就不會渲染。
留言改變了,ItemCount 會渲染嗎?
不會。它的 selector 仍回傳相同的商品數量,選取結果相等,所以不會請 React 渲染它。
const useCart = restate({ items: ['Book'], coupon: '', note: '' });
function ItemCount() {
// Re-renders only when the number of items changes.
const count = useCart((cart) => cart.items.length);
return <p>{count} items</p>;
}動手試試
修改欄位,觀察渲染計數器。關閉 shallow,看看每次都回傳新物件的 selector 會怎樣。
正在準備範例…
以 shallow 選取多個欄位
組出物件的 selector 每次都會回傳新物件,所以 Object.is 總是認為有變化。把 shallow 作為第二個參數傳入:它會逐一比較物件本身的欄位(只比較一層)。
shallow 也會對陣列、Map 與 Set 的元素做一層比較。
import { restate, shallow } from '@kkwaenggwari/restate';
function CartSummary() {
const { count, coupon } = useCart(
(cart) => ({ count: cart.items.length, coupon: cart.coupon }),
shallow,
);
return (
<p>
{count} items · {coupon || 'no coupon'}
</p>
);
}自訂比較函式
任何 (left, right) => boolean 函式都可以當作第二個參數。在 shallow 不夠用時使用,例如依 id 比較清單。
const ids = useCart(
(cart) => cart.items.map((item) => item.id),
(left, right) => left.length === right.length && left.every((id, index) => id === right[index]),
);用 batch 合併變化
每次 set 都會立即通知訂閱者。當一個動作要改動多個 store 時,用 batch 包起來:記憶體會立即更新,訂閱者則在回呼結束後只執行一次。
import { batch } from '@kkwaenggwari/restate';
function checkout() {
batch(() => {
useCart.set((cart) => ({ ...cart, items: [] }));
useOrder.set((order) => ({ ...order, status: 'paid' }));
});
// Subscribers run once, after both stores changed.
}在 React 之外訂閱
subscribeSelector 只在選取的值改變時呼叫你,同樣支援選用的比較函式。
const stop = useCart.subscribeSelector(
(cart) => cart.items.length,
(count, previous) => analytics.track('cart-size', { count, previous }),
);串接外部事件
restate 的第二個參數可以是 updater。它會在第一個元件或訂閱者開始監聽時啟動,在最後一個停止監聽時執行清理,所以瀏覽器事件、socket 或感測器只在需要時才連接。
用 start() 與 stop() 手動控制它。
import { restate } from '@kkwaenggwari/restate';
// Listens while at least one component or subscriber uses the store.
export const useOnline = restate(true, ({ setState }) => {
const update = () => setState(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
update();
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
});