Home >Web Front-end >JS Tutorial >Event-Driven Architecture for Clean React Component Communication
Are you tired of the endless tangle of props drilling and callback chains in your React applications? Does managing state and communication between deeply nested components feel like wrestling with spaghetti code?
An event-driven architecture can simplify your component interactions, reduce complexity, and make your app more maintainable. In this article, I’ll show you how to use a custom useEvent hook to decouple components and improve communication across your React app.
Let me walk you through it, let's start from
In modern application development, managing state and communication between components can quickly become cumbersome. This is especially true in scenarios involving props drilling—where data must be passed down through multiple levels of nested components—and callback chains, which can lead to tangled logic and make code harder to maintain or debug.
These challenges often create tightly coupled components, reduce flexibility, and increase the cognitive load for developers trying to trace how data flows through the application. Without a better approach, this complexity can significantly slow down development and lead to a brittle codebase.
In a typical React application, parent components pass props to their children, and children communicate back to the parent by triggering callbacks. This works fine for shallow component trees, but as the hierarchy deepens, things start to get messy:
Props Drilling: Data must be passed down manually through multiple levels of components, even if only the deepest component needs it.
Callback Chains: Similarly, child components must forward event handlers up the tree, creating tightly coupled and hard-to-maintain structures.
Take this scenario, for example:
This setup becomes harder to manage as the application grows. Intermediate components often act as nothing more than middlemen, forwarding props and callbacks, which bloats the code and reduces maintainability.
To address props drilling, we often turn to solutions like global state management libraries (e.g., Zustand) to streamline data sharing. But what about managing callbacks?
This is where an event-driven approach can be a game-changer. By decoupling components and relying on events to handle interactions, we can significantly simplify callback management. Let’s explore how this approach works.
Instead of relying on direct callbacks to communicate up the tree, an event-driven architecture decouples components and centralizes communication. Here’s how it works:
When SubChildren N triggers an event (e.g., onMyEvent), it doesn’t directly call a callback in the Parent.
Instead, it dispatches an event that is handled by a centralized Events Handler.
The Events Handler listens for the dispatched event and processes it.
It can notify the Parent (or any other interested component) or trigger additional actions as required.
Props are still passed down the hierarchy, ensuring that components receive the data they need to function.
This can be solved with centralized state management tools like zustand, redux, but will not be covered in this article.
But, how do we implement this architecture?
Let's create a custom hook called useEvent, this hook will be responsible of handling event subscription and returning a dispatch function to trigger the target event.
As I am using typescript, I need to extend the window Event interface in order to create custom events:
interface AppEvent<PayloadType = unknown> extends Event { detail: PayloadType; } export const useEvent = <PayloadType = unknown>( eventName: keyof CustomWindowEventMap, callback?: Dispatch<PayloadType> | VoidFunction ) => { ... };
By doing so, we can define custom events map and pass custom parameters:
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 ) => { ... };
Now that we defined needed interfaces, let's see the final hook code
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 }; };
The useEvent hook is a custom React hook for subscribing to and dispatching custom window events. It allows you to listen for custom events and trigger them with a specific payload.
What we are doing here is pretty simple, we are using the standard event management system and extending it in order to accommodate our custom events.
interface AppEvent<PayloadType = unknown> extends Event { detail: PayloadType; } export const useEvent = <PayloadType = unknown>( eventName: keyof CustomWindowEventMap, callback?: Dispatch<PayloadType> | VoidFunction ) => { ... };
Ok cool but, what about a
Check out this StackBlitz (if it does not load, please check it here)
This simple example showcases the purpose of the useEvent hook, basically the body's button is dispatching an event that is intercepted from Sidebar, Header and Footer components, that updates accordingly.
This let us define cause/effect reactions without the need to propagate a callback to many components.
Here are some real-world use cases where the useEvent hook can simplify communication and decouple components in a React application:
A notification system often requires global communication.
Scenario:
Solution: Use the useEvent hook to dispatch an onNotification event with the notification details. Components like the NotificationBanner and Header can listen to this event and update independently.
When a user toggles the theme (e.g., light/dark mode), multiple components may need to respond.
Scenario:
Benefits: No need to pass the theme state or callback functions through props across the entire component tree.
Implement global shortcuts, such as pressing "Ctrl S" to save a draft or "Escape" to close a modal.
Applications like chat apps or live dashboards require multiple components to react to real-time updates.
For complex forms with multiple sections, validation events can be centralized.
Track user interactions (e.g., button clicks, navigation events) and send them to an analytics service.
For collaborative tools like shared whiteboards or document editors, events can manage multi-user interactions.
By leveraging the useEvent hook in these scenarios, you can create modular, maintainable, and scalable applications without relying on deeply nested props or callback chains.
Events can transform the way you build React applications by reducing complexity and improving modularity. Start small—identify a few components in your app that would benefit from decoupled communication and implement the useEvent hook.
With this approach, you’ll not only simplify your code but also make it easier to maintain and scale in the future.
Why Use Events?
Events shine when you need your components to react to something that happened elsewhere in your application, without introducing unnecessary dependencies or convoluted callback chains. This approach reduces the cognitive load and avoids the pitfalls of tightly coupling components.
My Recommendation
Use events for inter-component communication—when one component needs to notify others about an action or state change, regardless of their location in the component tree.
Avoid using events for intra-component communication, especially for components that are closely related or directly connected. For these scenarios, rely on React's built-in mechanisms like props, state, or context.
A Balanced Approach
While events are powerful, overusing them can lead to chaos. Use them judiciously to simplify communication across loosely connected components, but don’t let them replace React’s standard tools for managing local interactions.
The above is the detailed content of Event-Driven Architecture for Clean React Component Communication. For more information, please follow other related articles on the PHP Chinese website!