React 18 - useSyncExternalSource
2022-01-12
Engineering
3 MIN

當 useMutableSource 遇上 redux

起因在當初 useMutableSource 的範例時:

import { useSelector } from "react-redux";

function Example() {
  // The user-provided selector should be memoized with useCallback.
  // This will prevent unnecessary re-subscriptions each update.
  // This selector can also use e.g. props values if needed.
  const memoizedSelector = useCallback(state => state.users, []);
  
  // The Redux hook will connect user code to useMutableSource.
  const users = useSelector(memoizedSelector);

  // ...
}

原本建議要把 getSnapshot 這個 function memoize 起來,不然會導致每次 re-render 的時候都會 re-subscription。

結果這造成了廣泛的討論,有人認為是過早最佳化。

另外一個問題是,在大家現有的 codebase 裏面,早就充滿了使用匿名 function 的 useSelector

const todos = useSelector(state => state.todos)

const todo = useSelector(state => selectTodoById(state, id))

如果要用新的 useMutableSource 要把舊的 redux code 全部都補上 useCallback ,實在太辛苦了。

Concurrent reads, synchronous updates

另一個問題是,在 react 18 之後用 fiber 實作了稱之為最小單位為 fiber 的更新方式,所以一次 render 有可能拆分成多個 fiber,那麼用到外部狀態時問題就來了,有可能前一個 fiber 跟後面一個fiber時 外部狀態已經改變,這時會直接觸發 subsciption 那麼這個 callback 跟 fiber 的順序就變得不可靠。

useMutableSource → useSyncExternalSource

經過了眾多的討論,為了讓外部 store 的更新是可靠的,最後決定了讓這個更新退回成同步的更新,

在他們新的設計下:

  • store change 觸發的更新會是同步的 , 就算有用 startTransition

  • 作為交換,built-in 的狀態更新絕對不會被 subscription 蓋掉,就算他們在同個 render 下接收

為了更好的符合這個改動,就決定把原本的 useMutableSource 改名了。

API Overview

import {useSyncExternalStore} from 'react';

// We will also publish a backwards compatible shim
// It will prefer the native API, when available
import {useSyncExternalStore} from 'use-sync-external-store/shim';

// Basic usage. getSnapshot must return a cached/memoized result
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

新版的 api 不用告訴它 store 在哪,而是直接告訴它怎麼拿到資料的快照 getSnapshot 。

// Name of API is not final
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';
const selection = useSyncExternalStoreWithSelector(
  store.subscribe,
  store.getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
);

並且給出了額外的 api 供使用。

結論

目前感覺這個 api 還很不穩定,根據 react conf 目前 Redux 確定會用 useSyncExternalStore 改寫,但它們改寫的過程中會不會再遇到問題導致 api 要修改就不得而知了。

另外這次很大一部分問題是 react 18 用了 fiber 後 render 不再 blocking ,當 render 交出控制權的時候,外部資料更新導致到底要如何處理的問題。

參考資料

https://github.com/reactwg/react-18/discussions/86 https://www.zhihu.com/question/502917860

Copyright © 2020-2023 | Design by Waiting7777