用于清洁 React 组件通信的事件驱动架构
你是否厌倦了 React 应用中无休止的 props 查询和回调链?管理深层嵌套组件之间的状态和通信是否像在与意大利面条式代码搏斗?
事件驱动架构可以简化组件交互,降低复杂性,并使您的应用更易于维护。在本文中,我将向您展示如何使用自定义useEvent
钩子来解耦组件并改善整个 React 应用的通信。
让我带你了解一下,让我们从
注意:此机制并非旨在替代全局状态管理系统,而是组件通信流的替代方法。
问题:Props 钻取和回调链
在现代应用程序开发中,管理组件之间的状态和通信很快就会变得繁琐。在涉及props 钻取(数据必须通过多层嵌套组件向下传递)和回调链的场景中尤其如此,这会导致逻辑混乱,使代码更难维护或调试。
这些挑战通常会造成组件紧密耦合,降低灵活性,并增加开发人员追踪数据在应用程序中流动方式的认知负担。如果没有更好的方法,这种复杂性会显著减慢开发速度,并导致代码库脆弱。
传统流程:Props 向下,回调向上
在典型的 React 应用中,父组件会将 props 传递给子组件,子组件则通过触发回调函数与父组件进行通信。对于浅层组件树来说,这种方式运行良好,但随着层级结构的加深,事情开始变得混乱:
Props Drilling:数据必须通过多个级别的组件手动传递,即使只有最深层的组件需要它。
回调链:同样,子组件必须将事件处理程序沿树向上转发,从而创建紧密耦合且难以维护的结构。
常见问题:回调复杂性
以这个场景为例:
- 父级将 props 传递给子级 A。
- 从那里开始,道具被深入到GrandChildren A/B并最终到SubChildren N。
- 如果SubChildren N需要通知 Parent 某个事件,它会触发一个回调,该回调会通过每个中间组件向上传递。
随着应用程序的增长,这种设置变得越来越难以管理。中间组件通常只充当中间人的角色,转发 props 和回调,这会使代码臃肿并降低可维护性。
为了解决props 钻取问题,我们经常会使用全局状态管理库(例如Zustand)之类的解决方案来简化数据共享。但是如何管理回调呢?
这正是事件驱动方法能够改变现状的地方。通过解耦组件并依赖事件来处理交互,我们可以显著简化回调管理。让我们来探索一下这种方法的工作原理。
解决方案:采用事件驱动方法
事件驱动架构不再依赖直接回调进行树状结构上的通信,而是解耦组件并集中通信。其工作原理如下:
事件调度
当SubChildren N触发事件(例如 onMyEvent)时,它不会直接调用 Parent 中的回调,
而是调度一个事件,并由集中式的 Events Handler 进行处理。
集中处理
事件处理器监听并处理分派的事件。
它可以通知父组件(或任何其他感兴趣的组件),或根据需要触发其他操作。
道具保持向下
Props 仍然会沿着层次结构传递,确保组件接收到运行所需的数据。
这可以通过 zustand、redux 等集中式状态管理工具来解决,但本文不会涉及。
执行
但是,我们如何实现这个架构?
useEvent 钩子
让我们创建一个名为useEvent的自定义钩子,这个钩子将负责处理事件订阅并返回一个调度函数来触发目标事件。
由于我使用的是 TypeScript,因此我需要扩展窗口Event
界面以创建自定义事件:
interface AppEvent<PayloadType = unknown> extends Event {
detail: PayloadType;
}
export const useEvent = <PayloadType = unknown>(
eventName: keyof CustomWindowEventMap,
callback?: Dispatch<PayloadType> | VoidFunction
) => {
...
};
通过这样做,我们可以定义自定义事件图并传递自定义参数:
interface AppEvent<PayloadType = unknown> extends Event {
detail: PayloadType;
}
export interface CustomWindowEventMap extends WindowEventMap {
/* Custom Event */
onMyEvent: AppEvent<string>; // an event with a string payload
}
export const useEvent = <PayloadType = unknown>(
eventName: keyof CustomWindowEventMap,
callback?: Dispatch<PayloadType> | VoidFunction
) => {
...
};
现在我们定义了所需的接口,让我们看看最终的钩子代码
import { useCallback, useEffect, type Dispatch } from "react";
interface AppEvent<PayloadType = unknown> extends Event {
detail: PayloadType;
}
export interface CustomWindowEventMap extends WindowEventMap {
/* Custom Event */
onMyEvent: AppEvent<string>;
}
export const useEvent = <PayloadType = unknown>(
eventName: keyof CustomWindowEventMap,
callback?: Dispatch<PayloadType> | VoidFunction
) => {
useEffect(() => {
if (!callback) {
return;
}
const listener = ((event: AppEvent<PayloadType>) => {
callback(event.detail); // Use `event.detail` for custom payloads
}) as EventListener;
window.addEventListener(eventName, listener);
return () => {
window.removeEventListener(eventName, listener);
};
}, [callback, eventName]);
const dispatch = useCallback(
(detail: PayloadType) => {
const event = new CustomEvent(eventName, { detail });
window.dispatchEvent(event);
},
[eventName]
);
// Return a function to dispatch the event
return { dispatch };
};
该useEvent
钩子是一个自定义的 React 钩子,用于订阅和调度自定义窗口事件。它允许您监听自定义事件并使用特定的有效负载触发它们。
我们在这里所做的非常简单,我们使用标准事件管理系统并对其进行扩展以适应我们的自定义事件。
参数:
eventName
(字符串):要监听的事件的名称。callback
(可选):事件触发时调用的函数,接收有效负载作为参数。
特征:
- 事件监听器:它监听指定的事件并调用
callback
该事件提供的函数detail
(自定义有效负载)。 - 调度事件:钩子提供了一个
dispatch
使用自定义有效负载触发事件的功能。
例子:
const { dispatch } = useEvent("onMyEvent", (data) => console.log(data));
// To dispatch an event
dispatch("Hello, World!");
// when dispatched, the event will trigger the callback
好的很酷但是,
现实世界的例子?
查看此 StackBlitz(如果它没有加载,请在此处查看)
这个简单的例子展示了钩子的用途useEvent
,基本上,主体的按钮正在调度从侧边栏、页眉和页脚组件拦截的事件,并进行相应的更新。
这让我们可以定义因果反应,而无需将回调传播到许多组件。
注意
正如评论中指出的那样,请记住记住使用的回调函数,useCallback
以避免连续的事件删除和创建,因为回调本身将是useEvent
内部 useEffect 的依赖项。
现实世界用例useEvent
以下是一些实际用例,其中useEvent
钩子可以简化通信并解耦 React 应用程序中的组件:
1.通知系统
通知系统通常需要全球通信。
-
设想:
- 当 API 调用成功时,需要在整个应用程序中显示“成功”通知。
- 标题中的“通知徽章”等组件也需要更新。
-
解决方案:使用
useEvent
钩子发送onNotification
包含通知详情的事件。 和 等组件NotificationBanner
可以Header
监听此事件并独立更新。
2.主题切换
当用户切换主题(例如,亮/暗模式)时,可能需要多个组件做出响应。
-
设想:
- 组件
ThemeToggle
调度自定义onThemeChange
事件。 - 侧边栏和标题等组件会监听此事件并相应地更新其样式。
- 组件
-
好处:不需要通过整个组件树中的 props 传递主题状态或回调函数。
3. 全局键绑定
实现全局快捷方式,例如按“Ctrl+S”保存草稿或按“Esc”关闭模式。
- 设想:
- 全局 keydown 监听器发送一个
onShortcutPressed
带有按键详细信息的事件。 - 模态组件或其他 UI 元素响应特定的快捷键,而无需依赖父组件来转发按键事件。
- 全局 keydown 监听器发送一个
4.实时更新
聊天应用程序或实时仪表板等应用程序需要多个组件来响应实时更新。
- 设想:
- 当新数据到达时,WebSocket 连接会调度
onNewMessage
或事件。onDataUpdate
- 聊天窗口、通知和未读消息计数器等组件可以独立处理更新。
- 当新数据到达时,WebSocket 连接会调度
5. 跨组件的表单验证
对于具有多个部分的复杂表单,可以集中验证事件。
- 设想:
onFormValidate
当用户填写字段时,表单组件会分派事件。- 摘要组件监听这些事件以显示验证错误,而无需与表单逻辑紧密耦合。
6.分析跟踪
跟踪用户交互(例如,按钮点击、导航事件)并将其发送到分析服务。
- 设想:
- 发送
onUserInteraction
带有相关详细信息的事件(例如,单击的按钮的标签)。 - 中央分析处理程序监听这些事件并将它们发送到分析 API。
- 发送
7.协作工具
对于共享白板或文档编辑器等协作工具,事件可以管理多用户交互。
- 设想:
onUserAction
每当用户绘制、键入或移动对象时调度事件。- 其他客户端和 UI 组件监听这些事件以实时反映变化。
useEvent
通过在这些场景中利用钩子,您可以创建模块化、可维护和可扩展的应用程序,而无需依赖深度嵌套的 props 或回调链。
结论
事件可以降低复杂性并提升模块化,彻底改变您构建 React 应用的方式。从小处着手——找出应用中一些可以从解耦通信中受益的组件,并实现 useEvent 钩子。
通过这种方法,您不仅可以简化代码,而且可以使其在将来更易于维护和扩展。
为什么要使用事件?
当你需要组件对应用程序中其他位置发生的事件做出响应时,事件就显得尤为重要,因为它不会引入不必要的依赖关系或复杂的回调链。这种方法可以减轻认知负担,并避免组件紧密耦合带来的陷阱。
我的建议:
使用事件进行组件间通信——当一个组件需要通知其他组件有关操作或状态更改时,无论它们在组件树中的位置如何。
避免使用事件进行组件内通信,尤其是对于紧密相关或直接连接的组件。对于这些情况,请依赖 React 的内置机制,例如 props、state 或 context。
平衡方法:
事件虽然功能强大,但过度使用可能会导致混乱。请谨慎使用事件来简化松散连接组件之间的通信,但不要让事件取代 React 管理本地交互的标准工具。