你是不是已经厌烦了 React 应用程序中无尽的 props 的层层传递和回调链条?管理状态和组件间的通信是否感觉像是在和纠缠的面条代码打交道?
一个事件驱动的架构可以简化你的组件交互,降低复杂性,并使你的应用程序更易于维护和管理。在这篇文章中,我将展示如何使用自定义的 useEvent
钩子来解耦组件以提高通信效率,并改善 React 应用程序中的组件通信。
让我带你一步步来,咱们从头开始
此处省略内容
问题:props 传递和钻取,回调链在现代应用程序开发中,特别是在涉及props穿透和回调链的情况下,管理状态和组件间通信很快就会变得繁琐。这会让逻辑变得十分混乱,使代码更难维护和调试。
这些挑战常常导致紧密耦合的组件,降低灵活性,并增加开发人员追踪数据在应用程序中流动的认知负担。如果没有更好的方法,这种复杂性可能会显著减慢开发速度,并导致一个脆弱的代码基础。
在典型的 React 应用中,父组件将 props 传递给子组件,子组件通过触发回调函数与父组件通信,。这种方法对于较浅的组件树效果很好,但随着层次加深,事情开始变得一团糟。
Props 钻孔:数据需要手动逐层传递给多个组件,即使只有最底层的组件才用得上。
回调链条:同样地,子组件也需要将事件处理程序传递给上层组件,这样就形成了紧密耦合且难以维护的架构。
比如说这种情况:
随着应用规模的扩大,这种设置变得越来越难以管理。中间组件经常只是作为中间人,传递属性和回调函数,这导致代码膨胀,降低了代码的可维护性。
为了处理props drilling问题,我们经常使用全局状态管理库(例如Zustand)来简化数据传递。那么,我们该如何处理回调呢?
这就是事件驱动方法大显身手的地方。通过解耦组件并利用事件来处理交互,我们可以大大简化回调管理。让我们来了解一下这种方法的具体操作。
……(此处表示分隔或占位)
解决方案:采用事件驱动方式:引入事件驱动方式
不再依赖直接回调来向上层传递信息,事件驱动的架构,解耦了组件,同时将通信集中起来。下面是如何运作的:
当 SubChildren N 触发一个事件(比如 onMyEvent)时,它不会直接调用父组件中的回调函数。
相反,它会将事件分发给集中的事件处理器来处理。
事件处理程序会监听并处理事件。
它可以通知父组件或任何其他感兴趣的组件,或在必要时触发其他行动。
属性仍然沿层级往下传,确保组件接收到他们运行所需的数据。
这可以用像 zustand 和 redux 这样的集中式状态管理工具来解决,但这篇文章不会涉及这些内容。
此处省略内容
我们该怎么实施这个架构呢?
useEvent
钩让我们创建一个名为 useEvent 的自定义钩(hook),这个钩子用来处理事件订阅,并返回一个触发目标事件的函数。
因为我使用的是 typescript,所以我需要扩展 window 对象上的 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 自定义窗口事件图 extends WindowEventMap { /* 自定义的事件 */ onMyEvent: AppEvent<string>; // 带有字符串负载的事件,例如 } export const useEvent = <PayloadType = unknown>( eventName: keyof 自定义窗口事件图, 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
(可选): 事件触发时调用的回调函数,该函数接收负载作为参数。callback
函数,传递事件的 detail
(自定义负载)参数。dispatch
函数来触发带有...的自定义负载的事件。const { dispatch } = useEvent("onMyEvent", (data) => console.log(data)); // 触发事件 dispatch("Hello, World!"); // 当触发事件时,回调会被调用
全屏(点击进入/退出)
好的,那关于一个呢?
有没有一个真实世界的例子?看看这个StackBlitz页面。如果无法加载,请在这里查看源代码这里。
基本上,这个简单的例子展示了useEvent
钩子的目的,页面中的按钮触发了一个事件,这个事件被侧边栏、顶部和底部组件捕获并相应地更新。
这让我们可以定义因果关系,而不需要在许多组件间传递回传函数。
注意
请注意,正如评论中提到的,请记得使用 useCallback
来缓存回调函数,以避免不断创建和删除事件,因为回调函数本身会成为 useEvent
内部 useEffect
的依赖项。
此处省略部分内容
useEvent
的真实应用场景
这里有一些实际应用场景,useEvent
钩子可以简化通信并使组件解耦,从而增强 React 应用中的组件管理。
通常,一个通知系统需要跨区域的沟通。
场景设定:
useEvent
钩子来触发带有通知详情的 onNotification
事件。比如 NotificationBanner
和 Header
这样的组件可以监听这个事件并自行更新。当用户切换主题(如浅/深模式)时,可能需要多个组件作出相应调整。
场景设定如下:
ThemeToggle
组件会派发出一个自定义的 onThemeChange
事件。实现全局快捷键,例如,按下“Ctrl+S ”来保存草稿内容,或者按下“Escape ”来关闭模态对话框。
场景:
全局的按键监听器会触发一个包含按键详情的 onShortcutPressed
事件。
例如聊天应用或动态仪表板这样的应用程序需要多个模块来响应实时数据更新。
场景:
当有新数据到达时,,WebSocket连接会触发onNewMessage
或onDataUpdate
事件。
对于复杂且包含多个部分的表单,可以将验证事件集中处理。
场景:
表单组件当用户填写字段时触发 onFormValidate
事件。
跟踪用户互动(例如,按钮点击、导航事件等),并将其发送给分析平台。
场景:
触发带有相关详情的 onUserInteraction
事件,例如点击的按钮标签。
对于协作工具如共享白板工具或文档编辑器,事件处理可以管理多用户互动。
场景:
当用户画图、打字或移动对象时,触发 onUserAction
事件。
通过在这些场景中利用 useEvent
钩子,你可以创建 更加模块化、易于维护和可扩展的应用程序,而无需深入处理 props 或回调链。
事件可以改变你构建React应用程序的方式,通过减少复杂性并增强模块化。从小处开始,比如,找出你的应用程序中几个可以从解耦通信中获益的组件,并实现useEvent钩子功能。
用这种方法,你不仅会简化你的代码,还会让它更容易在未来维护和扩展。这样做,让它在未来更易于维护和扩展。
为什么使用事件?
当需要让你的组件对应用程序其他地方发生的事情作出反应时,事件此时就显得特别有用,无需引入不必要的依赖或复杂的回调链。这种方法减少了认知负担,避免了组件之间紧密耦合的弊端,确保了术语使用的一致性和准确性。
我的建议
用事件来做组件之间的通信——当一个组件需要通知其他组件有关动作或状态的变化时,不论它们在组件树中的位置如何。
不要用事件来处理组件内部的通信,特别是那些紧密相连或直接关联的组件。对于这些场景,可以使用React自带的机制,比如 props、state 或 context。
平衡的方法
虽然事件很强大,但过度使用会导致混乱。合理使用事件来简化松散连接的组件间的通信,但不要用它们来替代React处理局部交互的标准方法。