[React 进阶系列] useSyncExternalStore hook

news2024/11/15 21:29:30

[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 的影响……

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot整合阿里云RocketMQ对接,商业版

1.需要阿里云开通商业版RocketMQ 普通消息新建普通主题,普通组,延迟消息新建延迟消息主题,延迟消息组 2.结构目录 3.引入依赖 <!--阿里云RocketMq整合--><dependency><groupId>com.aliyun.openservices</groupId><artifactId>ons-client</…

【C language】扫雷

目录 1.概要2.实现核心思想3.实现过程3.1游戏框架3.2游戏逻辑初始化棋盘 MineInit打印棋盘 MinePrint设置雷 SetMines扫雷 ClearMines 4.总结 1.概要 为了提高C初学者对C语言基本语法的掌控能力&#xff0c;一个极简版的扫雷游戏是十分适合锻炼代码能力的。下面分享仅用数组、…

14_Shell重定向输入输出

14_Shell重定向输入输出 输出重定向&#xff1a;一般情况&#xff0c;输出是在终端直接显示&#xff0c;改变输出位置&#xff0c;改变到文件中&#xff0c;这就是输出重定向 输入重定向&#xff1a;一般情况&#xff0c;输入是读取用户终端输入&#xff0c;改变输入位置&#…

026-GeoGebra中级篇-曲线(2)_极坐标曲线、参数化曲面、分段函数曲线、分形曲线、复数平面上的曲线、随机曲线、非线性动力系统的轨迹

除了参数曲线、隐式曲线和显式曲线之外&#xff0c;还有其他类型的曲线表示方法。本篇主要概述一下极坐标曲线、参数化曲面、分段函数曲线、分形曲线、复数平面上的曲线、随机曲线、和非线性动力系统的轨迹&#xff0c;可能没有那么深&#xff0c;可以先了解下。 目录 1. 极坐…

VScode:前端项目中yarn包的安装和使用

一、首先打开PowerShell-管理员身份运行ISE 输入命令&#xff1a; set-ExecutionPolicy RemoteSigned 选择“全是”&#xff0c;表示允许在本地计算机上运行由本地用户创建的脚本&#xff0c;没有报错就行了 二、接着打开VScode集成终端 输入 npm install -g yarn 再次输入以下…

IoT数据采集网关在企业应用中扮演的角色-天拓四方

随着物联网&#xff08;IoT&#xff09;技术的不断发展&#xff0c;越来越多的企业开始利用IoT技术实现智能化、自动化的生产和管理。在这个过程中&#xff0c;Iot数据采集网关作为连接物理世界与数字世界的桥梁&#xff0c;发挥着至关重要的作用。 IoT数据采集网关是一种硬件…

4.定时器

原理 时钟源&#xff1a;定时器是内部时钟源&#xff08;晶振&#xff09;&#xff0c;计数器是外部计时长度&#xff1a;对应TH TL计数器初值寄存器(高八位,低八位)对应的中断触发函数 中断源中断处理函数Timer0Timer0_Routine(void) interrupt 1Timer1Timer1_Routine(void) …

c++初阶知识——类和对象(中)

目录 1.类的默认成员函数 2.构造函数 3.析构函数 4.拷贝构造函数 5.运算符重载 5.1 赋值运算符重载 5.2 使用运算符重载等特性实现日期类 6.取地址运算符重载 6.1 const成员函数 6.2 取地址运算符重载 1.类的默认成员函数 默认成员函数就是⽤⼾没有显式实现&#…

网站开发:使用VScode安装yarn包和运行前端项目

一、首先打开PowerShell-管理员身份运行ISE 输入命令&#xff1a; set-ExecutionPolicy RemoteSigned 选择“全是”&#xff0c;表示允许在本地计算机上运行由本地用户创建的脚本&#xff0c;没有报错就行了 二、接着打开VScode集成终端 输入 npm install -g yarn 再次输入以…

防火墙双机热备带宽管理综合实验

拓扑图和要求如下&#xff1a; 之前的步骤可以去到上次的实验 1.步骤一&#xff1a; 首先在FW3防火墙上配置接口IP地址&#xff0c;划分区域 创建心跳线&#xff1a; 下面进行双机热备配置&#xff1a; 步骤二&#xff1a; 先将心跳线连接起来 注意&#xff1a;一定要将心跳…

Django select_related()方法

select_related()的作用 select_related()是Django ORM&#xff08;对象关系映射&#xff09;中的一种查询优化方法&#xff0c;主要用于减少数据库查询次数&#xff0c;提高查询效率。当你在查询一个模型实例时&#xff0c;如果这个实例有ForeignKey关联到其他模型&#xff0…

Hadoop-29 ZooKeeper集群 Watcher机制 工作原理 与 ZK基本命令 测试集群效果 3台公网云服务器

章节内容 上节我们完成了&#xff1a; ZNode的基本介绍ZNode节点类型的介绍事务ID的介绍ZNode实机测试效果 背景介绍 这里是三台公网云服务器&#xff0c;每台 2C4G&#xff0c;搭建一个Hadoop的学习环境&#xff0c;供我学习。 之前已经在 VM 虚拟机上搭建过一次&#xff…

Python JSON处理:兼容性与高级应用

JSON&#xff08;JavaScript Object Notation&#xff09;作为当前最流行的数据传输格式&#xff0c;在Python中也有多种实现方式。由于JSON的跨平台性和简便易用性&#xff0c;它在数据交互中被广泛应用。本文将重点讨论如何熟练应用Python的JSON库&#xff0c;将JSON数据映射…

【区块链 + 智慧政务】中国铁塔区块链委托代征开票应用 | FISCO BCOS应用案例

中国铁塔是全球规模最大的通信铁塔基础设施服务提供者。通信塔站址点多面广&#xff0c;业主构成复杂&#xff0c;因此产生海量税务、合同、票据等信息。为进一步提高场租或供电取票的及时性和规范性&#xff0c;严格遵循税务相关的要求&#xff0c;中国铁塔采用国产开源联盟链…

python--实验12

目录 知识点 第一部分&#xff1a;文件概述 第二部分&#xff1a;文件的基本操作 第三部分&#xff1a;目录管理 第四部分&#xff1a;CSV文件读写 第五部分&#xff1a;openpyxl等模块 小结 实验 知识点 第一部分&#xff1a;文件概述 文件标识&#xff1a;找到计算机…

尚硅谷大数据技术-数据湖Hudi视频教程-笔记03【Hudi集成Spark】

大数据新风口&#xff1a;Hudi数据湖&#xff08;尚硅谷&Apache Hudi联合出品&#xff09; B站直达&#xff1a;https://www.bilibili.com/video/BV1ue4y1i7na 尚硅谷数据湖Hudi视频教程百度网盘&#xff1a;https://pan.baidu.com/s/1NkPku5Pp-l0gfgoo63hR-Q?pwdyyds阿里…

【ARM】MDK-服务器与客户端不同网段内出现卡顿问题

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 记录不同网段之间的请求发送情况以及MDK网络版license文件内设置的影响。 2、 问题场景 客户使用很久的MDK网络版&#xff0c;在获取授权时都会出现4-7秒的卡顿&#xff0c;无法对keil进行任何操作&#xff0c;彻底…

Mac 如何安装vscode

Mac 电脑/ 苹果电脑如何安装 vscode 下载安装包 百度搜索vscode&#xff0c;即可得到vscode的官方下载地址&#xff1a;https://code.visualstudio.com/ 访问网页&#xff0c;点击下载即可。 下载完成后&#xff0c;得到下图所示的app。 将该 app 文件&#xff0c;放入到…

CV11_模型部署pytorch转ONNX

如果自己的模型中的一些算子&#xff0c;ONNX内部没有&#xff0c;那么需要自己去实现。 1.1 配置环境 安装ONNX pip install onnx -i https://pypi.tuna.tsinghua.edu.cn/simple 安装推理引擎ONNX Runtime pip install onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/si…

基于STM32设计的超声波测距仪(微信小程序)(186)

基于STM32设计的超声波测距仪(微信小程序)(186) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】ESP8266工作模式配置1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献1.4 开发工具的选择1.5 系统框架图…