useSyncExternalStore 的应用

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:修能

学而不思则罔,思而不学则殆 。 --- 《论语·为政》

What

useSyncExternalStore is a React Hook that lets you subscribe to an external store.

useSyncExternalStore 是一个支持让用户订阅外部存储的 Hook。官方文档


typescript 复制代码
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

Why

首先,我们这里基于 molecule1.x 的版本抽象了一个简易版的 mini-molecule。

typescript 复制代码
import { EventBus } from "../utils";

type Item = { key: string };
// 声明一个事件订阅
const eventBus = new EventBus();
// 声明模块数据类型
class Model {
  constructor(public data: Item[] = [], public current?: string) {}
}

export class Service {
  protected state: Model;
  constructor() {
    this.state = new Model();
  }

  setState(nextState: Partial<Model>) {
    this.state = { ...this.state, ...nextState };
    this.render(this.state);
  }

  private render(state: Model) {
    eventBus.emit("render", state);
  }
}
typescript 复制代码
export default function Home() {
  const state = useExternal();
  if (!state) return <div>loading...</div>;
  return (
    <>
      <strong>{state.current || "empty"}</strong>
      <ul>
        {state.data.map((i) => (
          <li key={i.key}>{i.key}</li>
        ))}
      </ul>
    </>
  );
}
typescript 复制代码
const service = new Service();
function useExternal() {
  const [state, setState] = useState<Model | undefined>(undefined);

  useEffect(() => {
    setState(service.getState());
    service.onUpdateState((next) => {
      setState(next);
    });
  }, []);

  return state;
}

如上面代码所示,已经实现了从外部存储获取相关数据,并且监听外部数据的更新,并触发函数组件的更新。

接下来实现更新外部数据的操作。

diff 复制代码
export default function Home() {
  const state = useExternal();
  if (!state) return <div>loading...</div>;
  return (
    <>
      <ul>
        {state.data.map((i) => (
          <li key={i.key}>{i.key}</li>
        ))}
      </ul>
+      <button onClick={() => service.insert(`${new Date().valueOf()}`)}>
+        add list
+      </button>
    </>
  );
}

其实要做的比较简单,就是增加了一个触发的按钮去修改数据即可。


上述这种比较简单的场景下所支持的 useExternal 写起来也是比较简单的。当你的场景越发复杂,你所需要考虑的就越多。就会导致项目的复杂度越来越高。而此时,如果有一个官方出品,有 React 团队做背书的 API 则会舒服很多。

以下是 useSyncExternlaStore 的 shim 版本相关代码:

javascript 复制代码
function useSyncExternalStore(subscribe, getSnapshot, // Note: The shim does not use getServerSnapshot, because pre-18 versions of
                              // React do not expose a way to check if we're hydrating. So users of the shim
                              // will need to track that themselves and return the correct value
                              // from `getSnapshot`.
                              getServerSnapshot) {
  {
    if (!didWarnOld18Alpha) {
      if (React.startTransition !== undefined) {
        didWarnOld18Alpha = true;

        error('You are using an outdated, pre-release alpha of React 18 that ' + 'does not support useSyncExternalStore. The ' + 'use-sync-external-store shim will not work correctly. Upgrade ' + 'to a newer pre-release.');
      }
    }
  } // Read the current snapshot from the store on every render. Again, this
  // breaks the rules of React, and only works here because of specific
  // implementation details, most importantly that updates are
  // always synchronous.


  var value = getSnapshot();

  {
    if (!didWarnUncachedGetSnapshot) {
      var cachedValue = getSnapshot();

      if (!objectIs(value, cachedValue)) {
        error('The result of getSnapshot should be cached to avoid an infinite loop');

        didWarnUncachedGetSnapshot = true;
      }
    }
  } // Because updates are synchronous, we don't queue them. Instead we force a
  // re-render whenever the subscribed state changes by updating an some
  // arbitrary useState hook. Then, during render, we call getSnapshot to read
  // the current value.
  //
  // Because we don't actually use the state returned by the useState hook, we
  // can save a bit of memory by storing other stuff in that slot.
  //
  // To implement the early bailout, we need to track some things on a mutable
  // object. Usually, we would put that in a useRef hook, but we can stash it in
  // our useState hook instead.
  //
  // To force a re-render, we call forceUpdate({inst}). That works because the
  // new object always fails an equality check.


  var _useState = useState({
    inst: {
      value: value,
      getSnapshot: getSnapshot
    }
  }),
    inst = _useState[0].inst,
    forceUpdate = _useState[1]; // Track the latest getSnapshot function with a ref. This needs to be updated
  // in the layout phase so we can access it during the tearing check that
  // happens on subscribe.


  useLayoutEffect(function () {
    inst.value = value;
    inst.getSnapshot = getSnapshot; // Whenever getSnapshot or subscribe changes, we need to check in the
    // commit phase if there was an interleaved mutation. In concurrent mode
    // this can happen all the time, but even in synchronous mode, an earlier
    // effect may have mutated the store.

    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({
        inst: inst
      });
    }
  }, [subscribe, value, getSnapshot]);
  useEffect(function () {
    // Check for changes right before subscribing. Subsequent changes will be
    // detected in the subscription handler.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({
        inst: inst
      });
    }

    var handleStoreChange = function () {
      // TODO: Because there is no cross-renderer API for batching updates, it's
      // up to the consumer of this library to wrap their subscription event
      // with unstable_batchedUpdates. Should we try to detect when this isn't
      // the case and print a warning in development?
      // The store changed. Check if the snapshot changed since the last time we
      // read from the store.
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({
          inst: inst
        });
      }
    }; // Subscribe to the store and return a clean-up function.


    return subscribe(handleStoreChange);
  }, [subscribe]);
  useDebugValue(value);
  return value;
}

How

针对上述例子进行改造

tsx 复制代码
const service = new Service();

export default function Home() {
  const state = useSyncExternalStore(
    (cb) => () => service.onUpdateState(cb),
    service.getState.bind(service)
  );

  if (!state) return <div>loading...</div>;
  return (
    <>
      <ul>
        {state.data.map((i) => (
          <li key={i.key}>{i.key}</li>
        ))}
      </ul>
      <button onClick={() => service.insert(`${new Date().valueOf()}`)}>
        add list
      </button>
    </>
  );
}

在 Molecule 中使用

tsx 复制代码
import { useContext, useMemo } from 'react';
import type { IMoleculeContext } from 'mo/types';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { Context } from '../context';

type Selector = keyof IMoleculeContext;
type StateType<T extends keyof IMoleculeContext> = ReturnType<IMoleculeContext[T]['getState']>;

export default function useConnector<T extends Selector>(selector: T) {
    const { molecule } = useContext(Context);
    const target = useMemo(() => molecule[selector], [molecule]);
    const subscribe = useMemo(() => {
        return (notify: () => void) => {
            target.onUpdateState(notify);
            return () => target.removeOnUpdateState(notify);
        };
    }, []);
    return useSyncExternalStore(subscribe, () => target.getState()) as StateType<T>;
}

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

相关推荐
郭wes代码13 小时前
纯 CSS+JS 实战:手搓一张支持自定义的通用金色电子奖状(万字详细拆解)
前端·javascript·css
竹枝溪18 小时前
从零吃透CSS(一):CSS基础语法与样式美化
前端·css·选择器·前端三剑客
用户059540174461 天前
把AI对话记忆测试从手动比对到Pytest+Redis自动化,验证效率提升了10倍,还顺带揪出3个隐蔽bug
前端·css
用户059540174463 天前
把AI聊天机器人记忆存储测试从3小时压到5分钟,我用Pytest + Docker搭了一套自动化回归
前端·css
STU学生网页设计4 天前
大一学生作品《前端框架开发技术》 期末网页制作 HTML+CSS+JavaScript 个人主页网页设计实例
css·前端框架·html
用户059540174464 天前
大模型长记忆评测踩坑实录:2000条错位记忆,让我排查了整整3小时
前端·css
用户059540174465 天前
LangChain Memory 踩坑实录:10种异常场景差点搞崩生产,我们写了一套自动化测试保命
前端·css
用户059540174465 天前
Redis缓存回滚踩坑实录:一个配置失误,让我在凌晨3点丢了3000条数据
前端·css
এ慕ོ冬℘゜6 天前
纯 CSS 实现自定义 Switch 开关(商品上下架滑块)
前端·css
并不喜欢吃鱼6 天前
二.前端web开发:零基础吃透 CSS 核心知识
前端·css