useSyncExternalStore

useSyncExternalStore允许应用订阅一个外部的 Store,但是这个 Store 需要是同步的。useSyncExternalStore的使用格式如下:

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);

useSyncExternalStore在使用的时候,最常用的用法是提供其中前两个函数,其中subscribe用来订阅 Store,并返回一个取消订阅的函数,getSnapshot用来从 Store 中读取数据。

Tip

第三个参数getServerSnapshot是在使用服务端渲染以及在客户端水合服务端渲染内容的时候用到。

以下实现一个可以被useSyncExternalStore订阅的 Store。

const itemsStore = {
  addItem(info) {
    items = [...items, { id: max(pluck("id", items)) + 1, text: info }];
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listener.filter((l) => l !== listener);
    };
  },
  getSnapshot() {
    return items;
  },
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

然后就可以使用useSyncExternalStore来订阅这个 Store 了。

function ItemList() {
  const items = useSyncExternalStore(
    itemsStore.subscribe,
    itemsStore.getSnapshot
  );

  return (
    <>
      <div>
        <button onClick={() => itemsStore.addItem(`${Math.random()}`)}>
          Add
        </button>
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{items.text}</li>
        ))}
      </ul>
    </>
  );
}

在使用useSyncExternalStore的时候需要注意以下几点:

  1. getSnapshot返回的 Store 快照内容必须是不可变的,而且在 Store 没有发生变化的时候,getSnapshot返回的内容应该始终是一致的。
  2. React 会使用Object.is来对比getSnapshot返回的内容,以决定是否需要重新渲染组件。
  3. 如果在重新渲染的时候传入了一个不同的subscribe函数,那么 React 会使用新传入的subscribe函数重新订阅 Store。所以为了避免subscribe函数发生改变,应该像上面示例中一样在组件外部定义订阅函数。
  4. 如果在非阻塞 transition 更新过程中更新了 Store,那么 React 将回退并视此次更新为阻塞更新。换句话说,在每次 transition 更新的时候,React 将在更改应用到 DOM 之前第二次调用getSnapshot,如果此次的返回值与之前的返回值不同,那么 React 将重新开始更新过程。
  5. 不要使用useSyncExternalStore返回的 Store 内容决定渲染状态。

例如根据上面最后一条的内容,下面的用法是不建议使用的。

function IllegalStoreSwitch() {
  const selectedItemId = useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );

  return selectedItemId != null ? <ItemDetailPage /> : <FeaturedItemPage />;
}