嫋嫋 發表於 2025-3-20 10:16:00

useSyncExternalStore 的应用

<blockquote>
<p>我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。</p>
</blockquote>
<blockquote>
<p>本文作者:修能</p>
</blockquote>
<p><em>学而不思则罔,思而不学则殆 。          --- 《论语·为政》</em></p>
<h1 id="what">What</h1>
<blockquote>
<p><code>useSyncExternalStore</code> is a React Hook that lets you subscribe to an external store.</p>
</blockquote>
<p><code>useSyncExternalStore</code> 是一个支持让用户订阅外部存储的 Hook。官方文档</p>
<hr>
<pre><code class="language-typescript">const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
</code></pre>
<hr>
<h1 id="why">Why</h1>
<p>首先,我们这里基于 molecule1.x 的版本抽象了一个简易版的 mini-molecule。</p>
<pre><code class="language-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&lt;Model&gt;) {
    this.state = { ...this.state, ...nextState };
    this.render(this.state);
}

private render(state: Model) {
    eventBus.emit("render", state);
}
}
</code></pre>
<pre><code class="language-typescript">export default function Home() {
const state = useExternal();
if (!state) return &lt;div&gt;loading...&lt;/div&gt;;
return (
    &lt;&gt;
      &lt;strong&gt;{state.current || "empty"}&lt;/strong&gt;
      &lt;ul&gt;
      {state.data.map((i) =&gt; (
          &lt;li key={i.key}&gt;{i.key}&lt;/li&gt;
      ))}
      &lt;/ul&gt;
    &lt;/&gt;
);
}
</code></pre>
<pre><code class="language-typescript">const service = new Service();
function useExternal() {
const = useState&lt;Model | undefined&gt;(undefined);

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

return state;
}
</code></pre>
<p>如上面代码所示,已经实现了从外部存储获取相关数据,并且监听外部数据的更新,并触发函数组件的更新。</p>
<p>接下来实现更新外部数据的操作。</p>
<pre><code class="language-diff">export default function Home() {
const state = useExternal();
if (!state) return &lt;div&gt;loading...&lt;/div&gt;;
return (
    &lt;&gt;
      &lt;ul&gt;
      {state.data.map((i) =&gt; (
          &lt;li key={i.key}&gt;{i.key}&lt;/li&gt;
      ))}
      &lt;/ul&gt;
+      &lt;button onClick={() =&gt; service.insert(`${new Date().valueOf()}`)}&gt;
+      add list
+      &lt;/button&gt;
    &lt;/&gt;
);
}

</code></pre>
<p>其实要做的比较简单,就是增加了一个触发的按钮去修改数据即可。</p>
<hr>
<p>上述这种比较简单的场景下所支持的 useExternal 写起来也是比较简单的。当你的场景越发复杂,你所需要考虑的就越多。就会导致项目的复杂度越来越高。而此时,如果有一个官方出品,有 React 团队做背书的 API 则会舒服很多。</p>
<p>以下是 useSyncExternlaStore 的 shim 版本相关代码:</p>
<pre><code class="language-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.inst,
    forceUpdate = _useState; // 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
      });
    }
}, );
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);
}, );
useDebugValue(value);
return value;
}
</code></pre>
<h1 id="how">How</h1>
<p>针对上述例子进行改造</p>
<pre><code class="language-tsx">const service = new Service();

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

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

</code></pre>
<hr>
<p><strong>在 Molecule 中使用</strong></p>
<pre><code class="language-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&lt;T extends keyof IMoleculeContext&gt; = ReturnType&lt;IMoleculeContext['getState']&gt;;

export default function useConnector&lt;T extends Selector&gt;(selector: T) {
    const { molecule } = useContext(Context);
    const target = useMemo(() =&gt; molecule, );
    const subscribe = useMemo(() =&gt; {
      return (notify: () =&gt; void) =&gt; {
            target.onUpdateState(notify);
            return () =&gt; target.removeOnUpdateState(notify);
      };
    }, []);
    return useSyncExternalStore(subscribe, () =&gt; target.getState()) as StateType&lt;T&gt;;
}
</code></pre>
<h2 id="最后">最后</h2>
<p>欢迎关注【袋鼠云数栈UED团队】~<br>
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star</p>
<ul>
<li><strong>大数据分布式任务调度系统——Taier</strong></li>
<li><strong>轻量级的 Web IDE UI 框架——Molecule</strong></li>
<li><strong>针对大数据领域的 SQL Parser 项目——dt-sql-parser</strong></li>
<li><strong>袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices</strong></li>
<li><strong>一个速度更快、配置更灵活、使用更简单的模块打包器——ko</strong></li>
<li><strong>一个针对 antd 的组件测试工具库——ant-design-testing</strong></li>
</ul><br><br>
来源:https://www.cnblogs.com/dtux/p/18782495
頁: [1]
查看完整版本: useSyncExternalStore 的应用