网站上那些轮播图视频怎么做的,总结格式模板,国内网站制作欣赏,技术培训学校机构[React 进阶系列] useSyncExternalStore hook
前情提要#xff0c;包括 yup 的实现在这里#xff1a;yup 基础使用以及 jest 测试
简单的提一下#xff0c;需要实现的功能是#xff1a;
yup schema 需要访问外部的 storage外部的 storage 是可变的React 内部也需要访问同…[React 进阶系列] useSyncExternalStore hook
前情提要包括 yup 的实现在这里yup 基础使用以及 jest 测试
简单的提一下需要实现的功能是
yup schema 需要访问外部的 storage外部的 storage 是可变的React 内部也需要访问同样的 storage
基于这几个前提条件再加上我们的项目已经从 React 17 升级到了 React 18因此就比较顺利的找到了一个新的 hookuseSyncExternalStore
这个新的 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]: SetFunction } {};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 EventEmitterOmitServerElement, type();Output() blueprintCreated new EventEmitterOmitServerElement, 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 的影响……