头图

事件驱动架构:打造清晰的React组件通信

原文链接:Event-Driven Architecture for Clean React Component Communication
作者:NicolaLC
译者:倔强青铜三

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

问题:Props Drilling和回调链

在现代应用开发中,组件间的状态管理和通信很快会变得繁琐。特别是在涉及Props Drilling的场景中——数据必须通过多层嵌套组件传递——以及回调链,这可能导致逻辑混乱,使代码更难维护或调试。

这些挑战经常导致组件紧密耦合,降低灵活性,并增加开发者追踪数据流经应用的心智负担。如果没有更好的方法,这种复杂性会显著拖慢开发速度,并导致脆弱的代码库

传统流程:Props向下,回调向上

在典型的React应用中,父组件向下传递Props给子组件,子组件通过触发回调向上与父组件通信。这对于浅层组件树来说没问题,但随着层级的加深,事情开始变得混乱:

Props Drilling:数据必须手动通过多个层级的组件传递,即使只有最深的组件需要它。

Callback Chains:同样,子组件必须将事件处理器向上传递,创建紧密耦合且难以维护的结构。

常见问题:回调复杂性

以这个场景为例:

  • 父组件向子组件A传递Props。
  • 然后,Props被钻取到孙组件A/B,最终到达子孙组件N
  • 如果子孙组件N需要通知父组件一个事件,它触发一个回调,该回调通过每个中间组件向上传递。

随着应用的增长,这种设置变得越来越难以管理。中间组件经常充当无用的中间人,转发Props和回调,这使代码膨胀,降低了可维护性。

img

为了解决Props Drilling问题,我们经常求助于全局状态管理库(例如,Zustand)来简化数据共享。但是,回调管理呢?

这就是事件驱动方法可以改变游戏规则的地方。通过解耦组件并依赖事件来处理交互,我们可以显著简化回调管理。让我们探索这种方法是如何工作的。

解决方案:事件驱动方法

img

与依赖直接回调向上通信不同,事件驱动架构解耦组件并集中通信。以下是它的工作原理:

事件派发

子孙组件N触发一个事件(例如,onMyEvent),它不会直接在父组件中调用回调。

相反,它派发一个由集中的事件处理器处理的事件。

集中处理

事件处理器监听派发的事件并处理它。

它可以通知父组件(或任何其他感兴趣的组件)或触发所需的其他操作。

Props保持向下

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 {
  /* 自定义事件 */
  onMyEvent: AppEvent<string>; // 一个带有字符串有效载荷的事件
}

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 {
  /* 自定义事件 */
  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); // 使用`event.detail`自定义有效载荷
    }) 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 { dispatch };
};

useEvent钩子是一个自定义的React钩子,用于订阅和派发自定义窗口事件。它允许您监听自定义事件,并用特定有效载荷触发它们。

我们在这里做的非常简单,我们使用标准事件管理系统并扩展它以适应我们的自定义事件。

参数:

  • eventName(字符串):要监听的事件名称。
  • callback(可选):当事件被触发时调用的函数,接收事件的detail(自定义有效载荷)作为参数。

特性:

  • 事件监听器:它监听指定的事件,并用事件的detail(自定义有效载荷)调用提供的callback
  • 派发事件:钩子提供了一个dispatch函数,用自定义有效载荷触发事件。

示例:

const { dispatch } = useEvent("onMyEvent", (data) => console.log(data));

// 派发事件
dispatch("Hello, World!");

// 派发时,事件将触发回调

好的,但是关于

真实世界的例子?

查看这个StackBlitz例子:
https://stackblitz.com/edit/event-drive-arch?embed=1&file=src...
<iframe src="https://stackblitz.com/edit/event-drive-arch?embed=1&amp;file=src%2FApp.tsx&amp;hideExplorer=1&amp;hideNavigation=1" width="100%" height="500" scrolling="no" frameborder="no" allowfullscreen="" allowtransparency="true" loading="lazy">
</iframe>

这个简单的例子展示了useEvent钩子的目的,基本上,body的按钮正在派发一个事件,该事件被Sidebar、Header和Footer组件拦截,并相应更新。

这让我们定义了因果反应,而无需将回调传播到许多组件。

注意

正如评论中指出的,请记住使用useCallback来记忆回调函数,以避免连续的事件移除和创建,因为回调本身将是useEvent内部useEffect的依赖项。

真实世界的useEvent用例

以下是一些真实世界的用例,其中useEvent钩子可以简化通信并在React应用中解耦组件:

1. 通知系统

通知系统通常需要全局通信。

  • 场景

    • 当API调用成功时,需要在应用中显示“成功”通知。
    • 像头部的“通知徽章”这样的组件也需要更新。
  • 解决方案:使用useEvent钩子派发带有通知详情的onNotification事件。像NotificationBannerHeader这样的组件可以独立监听此事件并更新。

2. 主题切换

当用户切换主题(例如,明/暗模式)时,可能需要多个组件响应。

  • 场景

    • ThemeToggle组件派发自定义onThemeChange事件。
    • 像Sidebar和Header这样的组件监听此事件并相应更新它们的样式。
  • 好处:无需通过整个组件树传递主题状态或回调函数。

3. 全局按键绑定

实现全局快捷方式,例如按“Ctrl+S”保存草稿或按“Escape”关闭模态框。

  • 场景

    • 全局keydown监听器派发带有按下键详情的onShortcutPressed事件。
    • 模态组件或其他UI元素响应特定快捷方式,而无需依赖父组件转发按键事件。

4. 实时更新

像聊天应用或实时仪表板这样的应用需要多个组件对实时更新做出反应。

  • 场景

    • WebSocket连接派发onNewMessageonDataUpdate事件,当新数据到达时。
    • 像聊天窗口、通知和未读消息计数器这样的组件可以独立处理更新。

5. 跨组件表单验证

对于具有多个部分的复杂表单,验证事件可以集中处理。

  • 场景

    • 表单组件在用户填写字段时派发onFormValidate事件。
    • 摘要组件监听这些事件以显示验证错误,而无需与表单逻辑紧密耦合。

6. 分析跟踪

跟踪用户交互(例如,按钮点击,导航事件)并将它们发送到分析服务。

  • 场景

    • 派发带有相关详情(例如,点击按钮的标签)的onUserInteraction事件。
    • 一个中央分析处理器监听这些事件并将它们发送到分析API。

7. 协作工具

对于像共享白板或文档编辑器这样的协作工具,事件可以管理多用户交互。

  • 场景

    • 每当用户绘制、打字或移动对象时,派发onUserAction事件。
    • 其他客户端和UI组件监听这些事件以实时反映更改。

通过在这些场景中利用useEvent钩子,您可以创建模块化、可维护和可扩展的应用,而无需依赖深层嵌套的Props或回调链。

结论

事件可以改变您构建React应用的方式,通过降低复杂性并提高模块化。从小处开始——确定您的应用中可以从解耦通信中受益的几个组件,并实现useEvent钩子。

通过这种方法,您不仅简化了代码,还使其更易于维护和扩展。

为什么使用事件?

当您的组件需要对应用程序的其他部分发生的事情做出反应时,事件会大放异彩,而无需引入不必要的依赖或复杂的回调链。这种方法减少了认知负担,并避免了组件紧密耦合的陷阱。

我的建议

当一个组件需要通知其他组件有关操作或状态更改时,无论它们在组件树中的位置如何,都使用事件进行组件间通信。

避免使用事件进行组件内通信,特别是对于紧密相关或直接连接的组件。对于这些场景,请依赖React的内置机制,如Props、状态或上下文。

平衡的方法

虽然事件功能强大,但过度使用它们可能会导致混乱。明智地使用它们来简化松散连接的组件之间的通信,但不要让它们取代React的标准工具,以管理本地交互。

最后感谢阅读,欢迎关注我,微信公众号:倔强青铜三

欢迎点赞收藏关注一键三连!!!


倔强青铜三
23 声望0 粉丝