我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:修能
学而不思则罔,思而不学则殆 。 --- 《论语·为政》
What
useSyncExternalStore is a React Hook that lets you subscribe to an external store.
useSyncExternalStore 是一个支持让用户订阅外部存储的 Hook。官方文档- const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
复制代码 Why
首先,我们这里基于 molecule1.x 的版本抽象了一个简易版的 mini-molecule。- 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);
- }
- }
复制代码- export default function Home() {
- const state = useExternal();
- if (!state) return loading...;
- return (
- <>
- <strong>{state.current || "empty"}</strong>
- <ul>
- {state.data.map((i) => (
- <li key={i.key}>{i.key}</li>
- ))}
- </ul>
- </>
- );
- }
复制代码- const service = new Service();
- function useExternal() {
- const [state, setState] = useState<Model | undefined>(undefined);
- useEffect(() => {
- setState(service.getState());
- service.onUpdateState((next) => {
- setState(next);
- });
- }, []);
- return state;
- }
复制代码 如上面代码所示,已经实现了从外部存储获取相关数据,并且监听外部数据的更新,并触发函数组件的更新。
接下来实现更新外部数据的操作。- export default function Home() {
- const state = useExternal();
- if (!state) return loading...;
- 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 版本相关代码:- 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
针对上述例子进行改造- const service = new Service();
- export default function Home() {
- const state = useSyncExternalStore(
- (cb) => () => service.onUpdateState(cb),
- service.getState.bind(service)
- );
- if (!state) return loading...;
- 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 中使用- 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
- 大数据分布式任务调度系统——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据领域的 SQL Parser 项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
- 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
- 一个针对 antd 的组件测试工具库——ant-design-testing
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |