useSyncExternalStore 的应用

news/2025/3/20 10:20:52/文章来源:https://www.cnblogs.com/dtux/p/18782495

我们是袋鼠云数栈 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 <div>loading...</div>;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 <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 版本相关代码:

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 <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 中使用

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/901908.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3.19 CW 模拟赛 T3. 软件工程

前言 策略肯定是锅了, 基础上需要对策略进行一些修改 喵了个咪的最终还是要针对考试 谢特 某吴姓同学的策略是非常适合我的, 在它的基础上, 我们考虑进行一些本土化 首先花 \(20 \textrm{min}\) 思考每道题, 也就是每道题严格 \(5 \textrm{ min}\) 首先按照能拿到的 \(\rm{subt…

生成AI的两大范式:扩散模型与Flow Matching的理论基础与技术比较

生成模型已成为人工智能领域的关键突破,赋予机器创建高度逼真的图像、音频和文本的能力。在众多生成技术中,扩散模型和Flow Matching尤为引人注目。这两种方法虽然都致力于在噪声与结构化数据之间建立转换,但其基础原理存在本质区别。本文将系统地比较这两种先进技术,深入探…

基于ACE_SOCK_Dgram的UDP同步通信

1、创建基于ACE_SOCK_DGRAM的UDP服务端1 void udp_server_base_on_synch()2 {3 // 1. 绑定服务端地址(端口 8080)4 ACE_INET_Addr server_addr(8080);5 ACE_SOCK_DGRAM sock;6 if (sock.open(server_addr) == -1) {7 std::cerr << "Serve…

第二十一章 项目管理科学基础(2025年详细解析版)

目录导学21.1 工程经济学资金的时间价值与等值计算定义常识现在值与将来值等值计算问题单利法与复利法 (利滚利)单利法复利法承兑汇票示例项目经济静态评价方法什么叫回收期?什么叫静态?静态投资回收期例题(必须掌握)投资收益率定义公式例题项目经济动态评价方法什么是动态…

windows输入法选用

前言 一直以来pc输入法都是用的搜狗,但是总想换一个用一用,每次都是尝试换讯飞,每次都用不下去。 不推荐 讯飞 bug极多。比如中文输入下,按shift,应该留下英文。当然一般情况下没问题,但是出现bug时,切换后再打字,会覆盖前面的字。 QQ 曾长期使用过,但是那时候用电脑用…

OpenTelemetry安装和使用

官网 https://opentelemetry.io/环境查看 系统环境# cat /etc/redhat-release Rocky Linux release 9.3 (Blue Onyx) # uname -a Linux Rocky9Opentelemetry003078 5.14.0-362.18.1.el9_3.0.1.x86_64 #1 SMP PREEMPT_DYNAMIC Sun Feb 11 13:49:23 UTC 2024 x86_64 x86_64 x86_…

大厂裁员不断,这个高薪岗位却找不到人?

《未来简史》写道:“未来属于那些能够快速适应变化、不断学习新技能的人。”大家好,我是陈哥。 当下,裁员潮席卷全球:微软裁撤万人级游戏部门,谷歌AI伦理团队被优化,亚马逊用机器人取代数万仓储岗位。然而,DevOps工程师的招聘却逆势而上。 据美国在线求职平台FlexJobs数…

跑酷P2 移动有害和切换关卡

跑酷游戏 第二集 本集中我们对上一集中的移动问题进行了优化,并且制作了关卡切换功能。 移动优化 上一集中,我们留下了一些移动方面的问题。首先是连跳问题,角色在空中可以不受限制的跳跃,我们需要解决一下。新建一个私有变量正在跳跃,用来存放角色跳跃的状态。在游戏开始…

跑酷P6 过关功能

跑酷游戏 第六集 本集我们实现了完成关卡的功能,并且修复了重新开始游戏后物资的bug。 角色绘制和显示逻辑 我们复制一下我们的物资角色,重命名为出口。然后绘制两个造型,一个是出口关闭的造型,一个是出口打开的造型。然后到我们的代码部分。我们的出口代码和物资角色的代码…

跑酷P6 关卡和金币系统

塔防游戏 第六集 本集主要实现了游戏的关卡处理和金币系统。 关卡处理 绘制一个开始按钮放置在画面左上方。我们希望在游戏开始时,或者一波关卡结束之后可以点击这个按钮,生成新一个关卡的敌人。我们新建一个全局变量关卡。游戏开始时关卡默认为0,每次开启一个新的关卡让这个…

跑酷P5 收集物资

跑酷游戏 第五集 本集我们实现了搜集物资的功能。 移动和复制 首先我们复制一遍场景角色,然后清空造型。复制的原因是我们移动和显示部分的代码逻辑,物资和场景是一样的。 然后我们来重新绘制造型。这里我们可以用自己喜欢的造型,金币,水果,宝石等都可以。这里我按视频的做…

读DAMA数据管理知识体系指南25数据集成活动

读DAMA数据管理知识体系指南25数据集成活动1. 规划和分析 1.1. 数据集成和互操作涉及在什么时间、什么地点、以什么方式能获得数据 1.2. 定义数据集成和生命周期需求1.2.1. 定义数据集成需求涉及理解组织的业务目标,以及为实现这些目标而需要的数据和建议的技术方案1.2.2. 数据…