[React 进阶系列] useSyncExternalStore hook
[React 进阶系列] useSyncExternalStore hook
前情提要,包括 yup 的实现在这里:yup 基础使用以及 jest 测试
简单的提一下,需要实现的功能是:
- yup schema 需要访问外部的 storage
- 外部的 storage 是可变的
- React 内部也需要访问同样的 storage
基于这几个前提条件,再加上我们的项目已经从 React 17 升级到了 React 18,因此就比较顺利的找到了一个新的 hook:useSyncExternalStore
这个新的 hook 可以监听到 React 外部 store——通常情况下可以是 local storage/session storage 这种——的变化,随后在 React 组件内部去更新对应的状态
官方文档其实解释的比较清楚了,使用 useSyncExternalStore
监听的 store 必须要实现以下两个功能:
-
subscribe
其作用是一个 subscriber,主要提供的功能在,当变化被监听到时,就会调用当前的 subscriber
我个人理解,相比于传统的 Consumer/Subscriber 模式,React 提供的这个 hook 是一个弱化的版本,subscriber 的主要目的是为了提示 React 这里有一个状态变化,所以很多情况下还是需要开发手动在
useEffect
中实现对应的功能当然,也是可以通过 event emitter 去出发 subscriber 的变化,这点还需要研究一下怎么实现
-
getSnapshot
这个是会被返回的最新状态
这也是 useSyncExternalStore
必须的两个参数。另一参数是为初始状态,为可选项:
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
实现 store
import { useSyncExternalStore } from "react";export class PrerequisiteStore {private prerequisite: string | undefined;private listeners: Set<() => void> = new Set();private initListeners: Set<() => void> = new Set();private isInitialized = false;subscribe(listener: () => void) {this.listeners.add(listener);return () => {this.listeners.delete(listener);};}getSnapshot() {return this.prerequisite;}setPrerequisite(prerequisite: string | undefined) {this.prerequisite = prerequisite;this.isInitialized = true;this.listeners.forEach((listener) => listener());this.initListeners.forEach((listener) => listener());this.initListeners.clear();}onInitialized(cb: () => void) {if (this.isInitialized) {cb();} else {this.initListeners.add(cb);}}
}const prerequisteStore = new PrerequisiteStore();export const getPrerequisite = () => prerequisteStore.getSnapshot();
export const setPrerequisite = (prerequisite: undefined | string) =>prerequisteStore.setPrerequisite(prerequisite);const subscribe = (cb: () => void) => prerequisteStore.subscribe(cb);
const getSnapshot = () => prerequisteStore.getSnapshot();
const getPrerequisiteSnapshot = getSnapshot;export const onPrerequisiteStoreInitialized = (cb: () => void) =>prerequisteStore.onInitialized(cb);export const usePrerequisiteSyncStore = () => {return useSyncExternalStore(subscribe, getSnapshot, getPrerequisiteSnapshot);
};
这个实现方法是用 class……其主要原因是想要基于一个 singleton 实现,这样全局访问 prerequisteStore
的时候只能访问这一个 store
不过同样的问题似乎也可以使用 object 来解决,就像 React 官方文档实现的那样:
// This is an example of a third-party store
// that you might need to integrate with React.// If your app is fully built with React,
// we recommend using React state instead.let nextId = 0;
let todos = [{ id: nextId++, text: "Todo #1" }];
let listeners = [];export const todosStore = {addTodo() {todos = [...todos, { id: nextId++, text: "Todo #" + nextId }];emitChange();},subscribe(listener) {listeners = [...listeners, listener];return () => {listeners = listeners.filter((l) => l !== listener);};},getSnapshot() {return todos;},
};function emitChange() {for (let listener of listeners) {listener();}
}
而且目前的实现实际上是无法自由绑定 listener
的,所以之后可能会修改一下这部分,而且还是需要花点时间琢磨一下 subscribe 这个功能怎么用
使用 store
错误实现
useEffect(() => {setTimeout(() => {setPrerequisite("A");initDemoSchema();}, 1000);setTimeout(() => {setPrerequisite("C");}, 2000);
}, []);useEffect(() => {console.log(prerequisiteStore, new Date().toISOString());if (prerequisiteStore) {const res = demoSchema.cast({});demoSchema.validate(res).then((res) => console.log(res)).catch((e) => {if (e instanceof ValidationError) {console.log(e.path, ",", e.message);}});}
}, [prerequisiteStore]);
这是 App.tsx
中的变化,实现效果如下:
这里可以看到有个问题,那就是在 useEffect(() => {}, [prerequisiteStore])
获取变化的时候,第一个 useEffect
没有获取更新的状态
修正
首先 store 的初始化,在当前的版本不是非常的必须,所以这里可以省略掉,直接保留 subscribe 等即可……不过因为测试代码已经添加了的关系,这里不会继续修改。主要就是修改一下 initDemoSchema
:
// 重命名
export const updateDemoSchema = (prerequisite: string | undefined) => {if (prerequisite) {demoSchema = demoSchema.shape({enumField: string().required().default(prerequisite).oneOf(Object.keys(getTestEnum() || [])),});}
};
随后在 App.tsx 中更新:
useEffect(() => {setTimeout(() => {setPrerequisite("A");}, 1000);setTimeout(() => {setPrerequisite("C");}, 2000);
}, []);useEffect(() => {console.log(prerequisiteStore, new Date().toISOString());if (prerequisiteStore) {updateDemoSchema(prerequisiteStore);const res = demoSchema.cast({});demoSchema.validate(res).then((res) => console.log(res)).catch((e) => {if (e instanceof ValidationError) {console.log(e.path, ",", e.message);}});}
}, [prerequisiteStore]);
这样就可以实现正常更新了:
补充:发现之前没有写 initDemoSchema
,之前旧的实现大致上没有特别大的区别,不过 prerequisite
的方式是通过 getPrerequisite
获取的。但是我没注意到的是,这只是一个 reference,同时也没有绑定 subscribe
,因此这里返回的永远是最初值,也就是在 initialized 后的值,也就是 A
下一步
下一步想做的就是把 schema 的变化抽离出来,并且尝试使用 todo 案例中的 emitChange
,这样 schema 的变化就不局限在 component 层级
虽然目前的业务情况来说,1 个 schema 基本上只会被用在 1 个页面上,不过还是想要将其剥离出来,减少对 react 组建的依赖性,而是直接想办法监听 store 的变化
测试代码
这个测试代码写的就比较含糊,基本上就是测试了一下 subscriber 被调用了几次
相对而言比较复杂的实现功能还是得回到 yup schema 去做……这等到实际上有这个需求再说吧,感觉那个写起来太痛苦了
import { PrerequisiteStore } from "../store/prerequisiteStore";describe("PrerequisiteStore", () => {let store: PrerequisiteStore;beforeEach(() => {store = new PrerequisiteStore();});test("should subscribe and unsubscribe listeners", () => {const listener = jest.fn();const unsubscribe = store.subscribe(listener);store.setPrerequisite("test");expect(listener).toHaveBeenCalledTimes(1);// 这里注意每个 subscribe 会返回的那个函数// 调用后就会 unsubscribe 当前行为unsubscribe();store.setPrerequisite("new test");expect(listener).toHaveBeenCalledTimes(1);});test("should return the current state with getSnapshot", () => {expect(store.getSnapshot()).toBeUndefined();store.setPrerequisite("test");expect(store.getSnapshot()).toBe("test");});test("should notify listeners when state changes", () => {const listener1 = jest.fn();const listener2 = jest.fn();store.subscribe(listener1);store.subscribe(listener2);store.setPrerequisite("test");expect(listener1).toHaveBeenCalledTimes(1);expect(listener2).toHaveBeenCalledTimes(1);});test("should handle initialization correctly", () => {const initListener = jest.fn();store.onInitialized(initListener);store.setPrerequisite("test");expect(initListener).toHaveBeenCalledTimes(1);const anotherInitListener = jest.fn();store.onInitialized(anotherInitListener);expect(anotherInitListener).toHaveBeenCalledTimes(1);});test("should clear initListeners after initialization", () => {const initListener = jest.fn();store.onInitialized(initListener);store.setPrerequisite("test");expect(initListener).toHaveBeenCalledTimes(1);store.setPrerequisite("new test");expect(initListener).toHaveBeenCalledTimes(1);});test("should handle multiple initialization listeners correctly", () => {const initListener1 = jest.fn();const initListener2 = jest.fn();store.onInitialized(initListener1);store.onInitialized(initListener2);store.setPrerequisite("test");expect(initListener1).toHaveBeenCalledTimes(1);expect(initListener2).toHaveBeenCalledTimes(1);});
});
event emitter
这里新增一下 event emitter 的实现:
class EventEmitter {private events: { [key: string]: Set<Function> } = {};on(event: string, listener: Function) {if (!this.events[event]) {this.events[event] = new Set();}this.events[event].add(listener);}off(event: string, listener: Function) {if (!this.events[event]) return;this.events[event].delete(listener);}emit(event: string, ...args: any[]) {if (!this.events[event]) return;for (const listener of this.events[event]) {listener(...args);}}
}const eventEmitter = new EventEmitter();
export default eventEmitter;
调用方法也很简单,在 schema 中实现:
eventEmitter.on("prerequisiteChange", updateDemoSchema);
app 中更新代码如下:
useEffect(() => {console.log("Prerequisite Store changed:",prerequisiteStore,new Date().toISOString());if (prerequisiteStore) {const res = demoSchema.cast({});demoSchema.validate(res).then((validatedRes) => console.log(validatedRes)).catch((e: ValidationError) => {console.log("Validation error:", e.path, e.message);});}
}, [prerequisiteStore]);
这样就可以有效的剥离 data schema 和 react component 之间的关系,而是通过事件触发进行正常的更新
最后渲染结果如下:
有的时候就不得不感叹 React 和 Angular 越到后面越有种……天下文章一大抄的感觉……
比如说这是之前学习 Angular 的 EventEmitter 的使用:
export class CockpitComponent {@Output() serverCreated = new EventEmitter<Omit<ServerElement, "type">>();@Output() blueprintCreated = new EventEmitter<Omit<ServerElement, "type">>();newServerName = "";newServerContent = "";onAddServer() {this.serverCreated.emit({name: this.newServerName,content: this.newServerContent,});}onAddBlueprint() {this.blueprintCreated.emit({name: this.newServerName,content: this.newServerContent,});}
}
学了一下 Angular 还真有助于理解 18 这个新 hook 的运用和延伸……
我感觉下意识的选择 class 可能也是受到了一点 Angular 的影响……
相关文章:

[React 进阶系列] useSyncExternalStore hook
[React 进阶系列] useSyncExternalStore hook 前情提要,包括 yup 的实现在这里:yup 基础使用以及 jest 测试 简单的提一下,需要实现的功能是: yup schema 需要访问外部的 storage外部的 storage 是可变的React 内部也需要访问同…...
Linux C++ 055-设计模式之状态模式
Linux C 055-设计模式之状态模式 本节关键字:Linux、C、设计模式、状态模式 相关库函数: 概念 状态模式(State Pattern)是设计模式的一种,属于行为模式。允许一个对象在其内部状态改变时改变它的行为。对象看起来似…...

景联文科技构建高质量心理学系知识图谱,助力大模型成为心理学科专家
心理大模型正处于快速发展阶段,在临床应用、教育、研究等多个领域展现出巨大潜力。 心理学系知识图谱能够丰富心理大模型的认知能力,使其在处理心理学相关问题时更加精确、可靠和有洞察力。这对于提高心理健康服务的质量和效率、促进科学研究以及优化教育…...

【数学建模】——数学规划模型
目录 一、线性规划(Linear Programming) 1.1 线性规划的基本概念 1.2 线性规划的图解法 模型建立: 二、整数规划(Integer Programming) 2.1 整数规划的基本概念 2.2 整数规划的求解方法 三、非线性规划&#x…...

卸载linux 磁盘的内容,磁盘占满
Linux清理磁盘 https://www.cnblogs.com/siyunianhua/p/17981758 当前文件夹下,数量 ls -l | grep "^-" | wc -l ls -lR | grep "^-" | wc -l 找超过100M的大文件 find / -type f -size 100M -exec ls -lh {} \; df -Th /var/lib/docker 查找…...

LeetCode-随机链表的复制
. - 力扣(LeetCode) 本题思路: 首先注意到随机链表含有random的指针,这个random指针指向是随机的;先一个一个节点的拷贝,并且把拷贝的节点放在拷贝对象的后面,再让拷贝节点的next指向原链表拷贝…...

axios 下载大文件时,展示下载进度的组件封装——js技能提升
之前面试的时候,有遇到一个问题:就是下载大文件的时候,如何得知下载进度,当时的回复是没有处理过。。。 现在想到了。axios中本身就有一个下载进度的方法,可以直接拿来使用。 下面记录一下处理步骤: 参考…...
Linux: network: device事件注册机制 chatGPT; notify
ChatGPT 在 Linux 内核中,有关网络设备(net-device)的事件注册机制,允许用户在网络设备的状态发生变化(例如设备被删除、添加或修改)时接收通知。这主要通过 netdev 事件通知机制实现。具体来说,内核提供了一组用于注册和处理网络设备事件的 API。 以下是一些关键组件…...
【ROS2】测试
为什么要进行自动化测试? 以下是我们应该进行自动化测试的许多重要原因之一: 您可以更快地对代码进行增量更新。ROS 有数百个包,具有许多相互依赖关系,因此很难预见一个小变化可能引起的问题。如果您的更改通过了单元测试…...
别卷模型,卷应用:从李彦宏的AI观点谈起
2024年7月4日,世界人工智能大会暨人工智能全球治理高级别会议在上海世博中心隆重召开。百度创始人、董事长兼首席执行官李彦宏在产业发展主论坛上的发言,引起了广泛关注。他提出:“大家不要卷模型,要卷应用!”这一观点…...
数据库(Database,简称DB)介绍
数据库(Database,简称DB)是信息技术领域中一个至关重要的组成部分,它按照数据结构来组织、存储和管理数据。以下是对数据库的详细介绍: 一、定义与基本概念 定义:数据库是按照数据结构来组织、存储和管理…...

Redis五种常用数据类型详解及使用场景
Redis 5 种基本数据类型 Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 这 5 种数据类型…...
Postman API测试覆盖率:全面评估指南
📊 Postman API测试覆盖率:全面评估指南 在API测试中,测试覆盖率是一个关键指标,它衡量了测试用例对代码的覆盖程度。Postman提供了多种工具和方法来评估API测试覆盖率,帮助开发者和测试人员确保API的质量和稳定性。本…...
C++--find
find 在[first,last)区间找第一个等于val的元素。 template<class InputIterator, class T> InputIterator find(InputIterator first,//起始迭代器 InputIterator last, //结束迭代器 const T& val); //需要查找的值 源码剖析 template<class InputI…...

JavaWeb入门程序解析(Spring官方骨架、配置起步依赖、SpringBoot父工程、内嵌Tomcat)
3.3 入门程序解析 关于web开发的基础知识,我们可以告一段落了。下面呢,我们在基于今天的核心技术点SpringBoot快速入门案例进行分析。 3.3.1 Spring官方骨架 之前我们创建的SpringBoot入门案例,是基于Spring官方提供的骨架实现的。 Sprin…...

mysql命令练习
创建数据表grade: CREATE TABLE grade( id INT NOT NULL, sex CHAR(1), firstname VARCHAR(20) NOT NULL, lastname VARCHAR(20) NOT NULL, english FLOAT, math FLOAT, chinese FLOAT ); 向数据表grade中插…...

AI绘画Stable Diffusion 零基础入门 —AI 绘画原理与工具介绍,万字解析AI绘画的使用教程
大家好,我是设计师阿威 想要入门 AI 绘画,首先需要了解它的原理是什么样的。 其实很早就已经有人基于深度学习模型展开了对图像生成的研究了,但在那时,生成的图像分辨率和内容都非常抽象。 直到近两年,AI 产出的图像…...

jenkins添加ssh证书
1、生成ssh密匙:windows生成ssh密匙-CSDN博客 2、添加添加ssh凭证:jenkins路由地址为:/manage/credentials/store/system/domain/_/ 点击添加凭证 选择第二个,将生成的私匙 id_rsa 里边的内容赋值到密钥,id留空自动…...
C++--accumulate介绍
在C中,accumulate是一个用于对容器中的元素进行累加操作的函数模板,位于 头文件中。它允许你对容器(如vector或array)中的元素进行累加运算,并返回累加的结果。 源代码展示 template<class InputIterator, class …...

C++写一个线程池
C写一个线程池 文章目录 C写一个线程池设计思路测试数据的实现任务类的实现线程池类的实现线程池构造函数线程池入口函数队列中取任务添加任务函数线程池终止函数 源码 之前用C语言写了一个线程池,详情请见: C语言写一个线程池 这次换成C了!…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...

【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...

协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...
全面解析数据库:从基础概念到前沿应用
在数字化时代,数据已成为企业和社会发展的核心资产,而数据库作为存储、管理和处理数据的关键工具,在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理,到社交网络的用户数据存储,再到金融行业的交易记录处理&a…...

基于开源AI智能名片链动2 + 1模式S2B2C商城小程序的沉浸式体验营销研究
摘要:在消费市场竞争日益激烈的当下,传统体验营销方式存在诸多局限。本文聚焦开源AI智能名片链动2 1模式S2B2C商城小程序,探讨其在沉浸式体验营销中的应用。通过对比传统品鉴、工厂参观等初级体验方式,分析沉浸式体验的优势与价值…...