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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

linux adb命令

⏩ 大家好哇&#xff01;我是小光&#xff0c;正在努力寻找自己的职业方向。 ⏩ 在调试设备时&#xff0c;经常会用到adb命令&#xff0c;本文对linux adb命令做一个知识分享。 ⏩ 感谢你的阅读&#xff0c;不对的地方欢迎指正。 1.adb命令 即 Android Debug Bridge 是一种允许…

解决第三方模块ts声明文件缺失的问题

最近小卷在用vite脚手架学习vue组件开发&#xff0c;使用的语言框架是typescript。在搭建vitepress在线文档服务时&#xff0c;用到了vitepress-demo-preview模块来展示vue组件示例和源代码。 发现import相关依赖时&#xff0c;会有这样的编译错误&#xff1a; 也就是没找到第…

Transformer模型:Postion Embedding实现

前言 这是对上一篇WordEmbedding的续篇PositionEmbedding。 视频链接&#xff1a;19、Transformer模型Encoder原理精讲及其PyTorch逐行实现_哔哩哔哩_bilibili 上一篇链接&#xff1a;Transformer模型&#xff1a;WordEmbedding实现-CSDN博客 正文 先回顾一下原论文中对Posit…

张量分解(5)——Tucker分解

&#x1f345; 写在前面 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;这里是hyk写算法了吗&#xff0c;一枚致力于学习算法和人工智能领域的小菜鸟。 &#x1f50e;个人主页&#xff1a;主页链接&#xff08;欢迎各位大佬光临指导&#xff09; ⭐️近…

【C++】构造函数详解

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

【开源 Mac 工具推荐之 1】gibMacOS:方便快捷的 macOS 完整包下载 Shell 工具

简介 gibMacOS 是由 GitHub 开发者 corpnewt 编写的一款 Shell 工具。它采用 Python 编程语言&#xff0c;可以让用户打开后在纯文本页面中轻松选择并下载来源于 Apple 官方的 macOS 完整安装包。 Repo 地址&#xff1a;https://github.com/corpnewt/gibMacOS &#xff08;其…

阿里通义音频生成大模型 FunAudioLLM 开源

简介 近年来&#xff0c;人工智能&#xff08;AI&#xff09;技术的进步极大地改变了人类与机器的互动方式&#xff0c;特别是在语音处理领域。阿里巴巴通义实验室最近开源了一个名为FunAudioLLM的语音大模型项目&#xff0c;旨在促进人类与大型语言模型&#xff08;LLMs&…

HTML+CSS博客文章列表

源代码在图片后面 点赞❤️收藏⭐️关注&#x1f495; 图示 感谢各位大佬支持 &#x1f618;&#x1f618;&#x1f618; 源代码 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>博…

解决ESLint和Prettier冲突的问题

在配置了ESLint的项目中使用Prettier进行格式化可能会出现冲突&#xff0c;不如Prettier配置了使用双引号&#xff0c;ESLint配置了单引号&#xff0c;当然可以一个一个改成一样的配置&#xff0c;但是比较麻烦。我发现可以直接使用ESLint的规则进行格式化。在VSCode配置过程如…

springmvc1

以前的servlet程序&#xff1a; springmvc 不同的处理器&#xff1a;不同的方法或者处理类 所有的请求都会经过dispathcherservlet的doservice方法&#xff1a; mvc原理&#xff1a; 前端控制器&#xff1a;jsp或者什么东西

AutoMQ 中的元数据管理

本文所述 AutoMQ 的元数据管理机制均基于 AutoMQ Release 1.1.0 版本 [1]。 01 前言 AutoMQ 作为新一代基于云原生理念重新设计的 Apache Kafka 发行版&#xff0c;其底层存储从传统的本地磁盘替换成了以对象存储为主的共享存储服务。对象存储为 AutoMQ 带来可观成本优势的…

【C++】初识C++(下)

前言 本篇博客继续总结一下C入门的一些小知识 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;C 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 ​ 目录 1.引用 1.1引用的概念 1.2const引用 1.3指针和引用的…

外包干了1个月,技术明显退步。。。

有一种打工人的羡慕&#xff0c;叫做“大厂”。 真是年少不知大厂香&#xff0c;错把青春插稻秧。 但是&#xff0c;在深圳有一群比大厂员工更庞大的群体&#xff0c;他们顶着大厂的“名”&#xff0c;做着大厂的工作&#xff0c;还可以享受大厂的伙食&#xff0c;却没有大厂…

软件测试面试题全网独家没有之一的资深测试工程师面试题集锦

1.自我介绍&#xff1f; 我是谁、工作几年、你上家公司做什么、负责什么、你的优势、为什么适合这个职位、我想做什么、在这个职位上想得到什么 有自信、不能吞吞吐吐 时间长度2-3分钟 2编写测试用例有哪几种方法&#xff1f; 等价类、边界值、因果图、流程分析、错误分析、…

【Pytorch】数据集的加载和处理(一)

Pytorch torchvision 包提供了很多常用数据集 数据按照用途一般分为三组&#xff1a;训练&#xff08;train&#xff09;、验证&#xff08;validation&#xff09;和测试&#xff08;test&#xff09;。使用训练数据集来训练模型&#xff0c;使用验证数据集跟踪模型在训练期间…

ectype:拓展ctype

拓展C库的ctype模块&#xff0c;将字节块或字符串进行分类或转换。

SQL Server 创建用户并授权

创建用户前需要有一个数据库&#xff0c;创建数据库命令如下&#xff1a; CREATE DATABASE [数据库名称]; CREATE DATABASE database1; 一、创建登录用户 方式1&#xff1a;SQL命令 命令格式&#xff1a;CREATE LOGIN [用户名] WITH PASSWORD 密码; 例如&#xff0c;创建…

全球DeepFake攻防挑战赛DataWhale AI 夏令营——图像赛道

全球DeepFake攻防挑战赛&DataWhale AI 夏令营——图像赛道 赛题背景 随着人工智能技术的迅猛发展&#xff0c;深度伪造技术&#xff08;Deepfake&#xff09;正成为数字世界中的一把双刃剑。这项技术不仅为创意内容的生成提供了新的可能性&#xff0c;同时也对数字安全构…

Mac 息屏不断网

这里息屏指的是屏幕不黑&#xff0c;屏幕黑了好像必断网 我的系统是 14.5 我调整了两个地方&#xff0c;一个是电池——选项——唤醒以供访问 另外一个地方是锁定屏幕——延长关闭显示器的时间&#xff08;让显示器不黑&#xff09;

如何批量删除重复数据?推荐两种方法

在日常的办公中&#xff0c;很多用户都会使用Excel。借助这款软件&#xff0c;用户可以完成对各种数据的处理。但很多时候我们会发现&#xff0c;同一张表格里有很多重复的数据&#xff0c;这或许会为统计带来错误。为此&#xff0c;我们就需要删除重复项才可以帮助我们很好的解…