找回密码
 立即注册
首页 业界区 业界 useSyncExternalStore 的应用

useSyncExternalStore 的应用

赵淳美 2025-6-6 09:55:12
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:修能
学而不思则罔,思而不学则殆 。          --- 《论语·为政》
What

useSyncExternalStore is a React Hook that lets you subscribe to an external store.
useSyncExternalStore 是一个支持让用户订阅外部存储的 Hook。官方文档
  1. const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
复制代码
Why

首先,我们这里基于 molecule1.x 的版本抽象了一个简易版的 mini-molecule。
  1. import { EventBus } from "../utils";
  2. type Item = { key: string };
  3. // 声明一个事件订阅
  4. const eventBus = new EventBus();
  5. // 声明模块数据类型
  6. class Model {
  7.   constructor(public data: Item[] = [], public current?: string) {}
  8. }
  9. export class Service {
  10.   protected state: Model;
  11.   constructor() {
  12.     this.state = new Model();
  13.   }
  14.   setState(nextState: Partial<Model>) {
  15.     this.state = { ...this.state, ...nextState };
  16.     this.render(this.state);
  17.   }
  18.   private render(state: Model) {
  19.     eventBus.emit("render", state);
  20.   }
  21. }
复制代码
  1. export default function Home() {
  2.   const state = useExternal();
  3.   if (!state) return loading...;
  4.   return (
  5.     <>
  6.       <strong>{state.current || "empty"}</strong>
  7.       <ul>
  8.         {state.data.map((i) => (
  9.           <li key={i.key}>{i.key}</li>
  10.         ))}
  11.       </ul>
  12.     </>
  13.   );
  14. }
复制代码
  1. const service = new Service();
  2. function useExternal() {
  3.   const [state, setState] = useState<Model | undefined>(undefined);
  4.   useEffect(() => {
  5.     setState(service.getState());
  6.     service.onUpdateState((next) => {
  7.       setState(next);
  8.     });
  9.   }, []);
  10.   return state;
  11. }
复制代码
如上面代码所示,已经实现了从外部存储获取相关数据,并且监听外部数据的更新,并触发函数组件的更新。
接下来实现更新外部数据的操作。
  1. export default function Home() {
  2.   const state = useExternal();
  3.   if (!state) return loading...;
  4.   return (
  5.     <>
  6.       <ul>
  7.         {state.data.map((i) => (
  8.           <li key={i.key}>{i.key}</li>
  9.         ))}
  10.       </ul>
  11. +      <button onClick={() => service.insert(`${new Date().valueOf()}`)}>
  12. +        add list
  13. +      </button>
  14.     </>
  15.   );
  16. }
复制代码
其实要做的比较简单,就是增加了一个触发的按钮去修改数据即可。
上述这种比较简单的场景下所支持的 useExternal 写起来也是比较简单的。当你的场景越发复杂,你所需要考虑的就越多。就会导致项目的复杂度越来越高。而此时,如果有一个官方出品,有 React 团队做背书的 API 则会舒服很多。
以下是 useSyncExternlaStore 的 shim 版本相关代码:
  1. function useSyncExternalStore(subscribe, getSnapshot, // Note: The shim does not use getServerSnapshot, because pre-18 versions of
  2.                               // React do not expose a way to check if we're hydrating. So users of the shim
  3.                               // will need to track that themselves and return the correct value
  4.                               // from `getSnapshot`.
  5.                               getServerSnapshot) {
  6.   {
  7.     if (!didWarnOld18Alpha) {
  8.       if (React.startTransition !== undefined) {
  9.         didWarnOld18Alpha = true;
  10.         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.');
  11.       }
  12.     }
  13.   } // Read the current snapshot from the store on every render. Again, this
  14.   // breaks the rules of React, and only works here because of specific
  15.   // implementation details, most importantly that updates are
  16.   // always synchronous.
  17.   var value = getSnapshot();
  18.   {
  19.     if (!didWarnUncachedGetSnapshot) {
  20.       var cachedValue = getSnapshot();
  21.       if (!objectIs(value, cachedValue)) {
  22.         error('The result of getSnapshot should be cached to avoid an infinite loop');
  23.         didWarnUncachedGetSnapshot = true;
  24.       }
  25.     }
  26.   } // Because updates are synchronous, we don't queue them. Instead we force a
  27.   // re-render whenever the subscribed state changes by updating an some
  28.   // arbitrary useState hook. Then, during render, we call getSnapshot to read
  29.   // the current value.
  30.   //
  31.   // Because we don't actually use the state returned by the useState hook, we
  32.   // can save a bit of memory by storing other stuff in that slot.
  33.   //
  34.   // To implement the early bailout, we need to track some things on a mutable
  35.   // object. Usually, we would put that in a useRef hook, but we can stash it in
  36.   // our useState hook instead.
  37.   //
  38.   // To force a re-render, we call forceUpdate({inst}). That works because the
  39.   // new object always fails an equality check.
  40.   var _useState = useState({
  41.     inst: {
  42.       value: value,
  43.       getSnapshot: getSnapshot
  44.     }
  45.   }),
  46.     inst = _useState[0].inst,
  47.     forceUpdate = _useState[1]; // Track the latest getSnapshot function with a ref. This needs to be updated
  48.   // in the layout phase so we can access it during the tearing check that
  49.   // happens on subscribe.
  50.   useLayoutEffect(function () {
  51.     inst.value = value;
  52.     inst.getSnapshot = getSnapshot; // Whenever getSnapshot or subscribe changes, we need to check in the
  53.     // commit phase if there was an interleaved mutation. In concurrent mode
  54.     // this can happen all the time, but even in synchronous mode, an earlier
  55.     // effect may have mutated the store.
  56.     if (checkIfSnapshotChanged(inst)) {
  57.       // Force a re-render.
  58.       forceUpdate({
  59.         inst: inst
  60.       });
  61.     }
  62.   }, [subscribe, value, getSnapshot]);
  63.   useEffect(function () {
  64.     // Check for changes right before subscribing. Subsequent changes will be
  65.     // detected in the subscription handler.
  66.     if (checkIfSnapshotChanged(inst)) {
  67.       // Force a re-render.
  68.       forceUpdate({
  69.         inst: inst
  70.       });
  71.     }
  72.     var handleStoreChange = function () {
  73.       // TODO: Because there is no cross-renderer API for batching updates, it's
  74.       // up to the consumer of this library to wrap their subscription event
  75.       // with unstable_batchedUpdates. Should we try to detect when this isn't
  76.       // the case and print a warning in development?
  77.       // The store changed. Check if the snapshot changed since the last time we
  78.       // read from the store.
  79.       if (checkIfSnapshotChanged(inst)) {
  80.         // Force a re-render.
  81.         forceUpdate({
  82.           inst: inst
  83.         });
  84.       }
  85.     }; // Subscribe to the store and return a clean-up function.
  86.     return subscribe(handleStoreChange);
  87.   }, [subscribe]);
  88.   useDebugValue(value);
  89.   return value;
  90. }
复制代码
How

针对上述例子进行改造
  1. const service = new Service();
  2. export default function Home() {
  3.   const state = useSyncExternalStore(
  4.     (cb) => () => service.onUpdateState(cb),
  5.     service.getState.bind(service)
  6.   );
  7.   if (!state) return loading...;
  8.   return (
  9.     <>
  10.       <ul>
  11.         {state.data.map((i) => (
  12.           <li key={i.key}>{i.key}</li>
  13.         ))}
  14.       </ul>
  15.       <button onClick={() => service.insert(`${new Date().valueOf()}`)}>
  16.         add list
  17.       </button>
  18.     </>
  19.   );
  20. }
复制代码
在 Molecule 中使用
  1. import { useContext, useMemo } from 'react';
  2. import type { IMoleculeContext } from 'mo/types';
  3. import { useSyncExternalStore } from 'use-sync-external-store/shim';
  4. import { Context } from '../context';
  5. type Selector = keyof IMoleculeContext;
  6. type StateType<T extends keyof IMoleculeContext> = ReturnType<IMoleculeContext[T]['getState']>;
  7. export default function useConnector<T extends Selector>(selector: T) {
  8.     const { molecule } = useContext(Context);
  9.     const target = useMemo(() => molecule[selector], [molecule]);
  10.     const subscribe = useMemo(() => {
  11.         return (notify: () => void) => {
  12.             target.onUpdateState(notify);
  13.             return () => target.removeOnUpdateState(notify);
  14.         };
  15.     }, []);
  16.     return useSyncExternalStore(subscribe, () => target.getState()) as StateType<T>;
  17. }
复制代码
最后

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

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册