DEV Community

Cover image for Event-Driven Architecture for Clean React Component Communication

Event-Driven Architecture for Clean React Component Communication

Nicola on December 04, 2024

Are you tired of the endless tangle of props drilling and callback chains in your React applications? Does managing state and communication between...
Collapse
 
vikkio88 profile image
Vincenzo

this looks like it will get messy quickly depending on what type of state you need to maintain.

a state management solution would be superior to this even if the state is simpler, less code and future proof in case it needs to grow.

Collapse
 
nicolalc profile image
Nicola

Hi Vincenzo, thanks for your comment. Are you talking about the cloned [count, setCount] state management? If so it's just for example purposes and not the right use-case for useEvent hook, it's just to demonstrate how to communicate between components and no callbacks at all. I will change the example if that's the case as I agree it might be confusing.

Collapse
 
vikkio88 profile image
Vincenzo

even if it wasn't just that, the event system doesn't really scale well for shared state.

Thread Thread
 
nicolalc profile image
Nicola

Indeed, as I wrote at the start of the article, to manage shared state there are solutions like Zustand or Redux, the event system is intended for communication, not for sharing data.

Think about it like: "Something happened elsewhere, and I need to react accordingly".

Hope this clarify its responsibility.

Thread Thread
 
chandragie profile image
chandragie

I was thinking the same as him. Thought that Redux etc would be enough. But I remember you highlighted the callback chains bottom up. Thanks for the example.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Do you not need deps on the useEvent? Or is it rebinding every redraw? I have a very similar system, and needed that for it.

Collapse
 
nicolalc profile image
Nicola

Thanks Mike for pointing that out, for sure something I need to add to the article.

As the callback itself is part of the useEffect deps array:

  useEffect(() => {
    [...]
  }, [callback, eventName]);
Enter fullscreen mode Exit fullscreen mode

it works like any classic hook dependency, and I suggest to memoize it else it will result in a new function at every re-render as in javascript functions are not comparable.

What I usually do is that (until React 19 compiler at least):

const myCallback = useCallback([...], [deps])

useEvent("eventName", myCallback);
Enter fullscreen mode Exit fullscreen mode

By doing so you will pass a memoized function to the hook, and if I'm not wrong (need to check that but pretty sure it works) the ref to that function will not change and the effect not triggered again.

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Yeah that's a way! I also ended up wiring the event to my global dispatcher in a useMemo and removing it in the release function of a useEffect to get it there quickly as I raise quite a few lifecycle events and occasionally they were missed. Due to this I also capture events before the component is mounted and then dispatch them when it is.

export function useEvent(eventName, handler, deps = []) {
    const mounted = useRef()
    const mountFn = useRef(noop)
    const remove = useRef(noop)
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const innerHandler = useCallback(readyHandler, [...deps, eventName])

    innerHandler.priority = handler.priority
    useMemo(() => {
        remove.current()
        remove.current = handle(eventName, innerHandler)
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...deps, eventName])
    useEffect(() => {
        mounted.current = true
        mountFn.current()
        return () => {
            if (remove.current) {
                remove.current()
                remove.current = noop
            }
            if (mounted.current) {
                mounted.current = false
            }
        }
    }, [eventName])
    return handler

    function readyHandler(...params) {
        const self = this
        if (mounted.current) {
            return handler.call(self, ...params)
        }
        const existing = mountFn.current
        mountFn.current = () => {
            try {
                existing()
            } catch (e) {
                //
            }
            handler.call(self, ...params)
        }
        return undefined
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alaindet profile image
Alain D'Ettorre

The browser-native CustomEvent is great and is also the standard in web components. However, keeping track of those is quite hard as they're not part of a component's props object. I still find them useful but I'm scared to use them and would probably rely on using a Context.

I believe using CustomEvent even skips rendering cycles and is easier to use compared to context, but still it would be unconventional and potentially non-maintainable for me.

Collapse
 
nicolalc profile image
Nicola

Thanks Alain for sharing your thoughts, in general in programming there is no best solution for everything, you need to find the best one for your needs and you personal developer experience. For me context are super good but I hate the doom they create when you have too many, and you need to create super contexts to avoid too complex context trees

Collapse
 
alaindet profile image
Alain D'Ettorre

You're right, Contexts can get ugly fast and I honestly don't like them much, BUT they are idiomatic to React and they're guaranteed to be the go-to first-party solution for sharing state and behavior, so it's ok for me.

About CustomEvent again, despite being great and standard, it's clearly not idiomatic in React and using a message bus that's external to fix an internal concern feels like a hack. My 2 cents

Thread Thread
 
nicolalc profile image
Nicola

Yep you’re right, keep in mind that this solution starts from a react “problem” but extends to an architecture that you can use everywhere. Keep in mind that you don’t need to fully replace react standards as this system can help you in some scenarios, but not for every one. As always you need to balance solutions based on problems.

For example, I like to use events when you open a table item details view in a modal that overlays the table, you edit the item and you need the table to refresh. In that case events are great as you can just say “an item has changed” and the table fetch data again. These two views might be not aware of each other depending on your implementation, so don’t take this approach as the best one for every case, but for some cases

Collapse
 
lucasmedina profile image
Lucas Medina

I like this approach. It's really interesting when dealing with state that's only used by a single component, but triggered from anywhere.
Maybe a good example would be status alerts, or UI notifications, since it's commonly triggered by various places but its data is only used by one component.

Collapse
 
panda_bc3691bedb3ab5e6 profile image
Panda张向北

Many thanks. This is a great idea

Collapse
 
businessdirectorysites profile image
Business Directory Sites

Great work!

Collapse
 
isocroft profile image
Okechukwu Ifeora

Hey there, Great article!

Would you be open to checking this out: npmjs.com/package/react-busser?

It is a package based on event-driven data communication using a hook useBus(). I built it for the purposes you have listed in your wonderful article. It has some building-block hooks for lists (arrays), composites (object literal) and properties (primitive types).

It also has other helper hooks too that are great for UI development.

Let me know what you thick

Collapse
 
nicolalc profile image
Nicola

Thanks dude, first of all, amazing job here!

There are soo many things that it will require quite some time in order to analyze everything, so don't be mad if I will take some time to review it.

Anything you would like me to check in particular? For instance code reviewing, logic reviewing or anything else?

Keep working on this!

Collapse
 
isocroft profile image
Okechukwu Ifeora

Thanks a lot. It's okay if you take time to review it.

The more, the better i'd say.

Yes, i would love that you help me with some logic reviewing specifically on 2 codesandbox code snippets that use the package (react-busser).

This first one is an example/sample of how the package (react-busser)) works without the need to pass too many props just like in your article.

It's a simple todo app and the way it works is depicted in this image.

The concept of how the events work is what is known as (i coined this) CASCADE BROADCAST. There is a source (component) for the events implemented as a source hook and there is a target (component) for the events implemented as a target hook.

The source and target hooks send and receive events respectively.

You can play around with the simple todo app in the preview section of the codesandbox link above.

The real logic issue comes with this second codesandbox. whose link is below this line.

This is a simple e-commerce page that also uses the package (react-busser) but there's a bug when i try to filter the list of product items with the search bar.

The functionality for adding a product to the cart works well.

I would like a logic review on what you think might be the issue.

Thanks a lot for this.

Collapse
 
nick_fe8c88c99b72333303a5 profile image
Nick

.localservice@runtime.c8c182a3.js:26
Warning: Cannot update a component (Sidebar) while rendering a different component (Body). To locate the bad setState() call inside Body, follow the stack trace as described in reactjs.org/link/setstate-in-render
at Body (eventdrivearch-oc1y--5173--c8c182a...)
at div
at App

Collapse
 
dsaga profile image
Dusan Petkovic

I mix of the 2 would also be a good solution, I actually use an event driven approach along with normal event callbacks, so that I can easily trigger global events like notifications, errors and easily capture them, also to decouple certain components and allow them to communicate and handle logic without needing to prop drill event handles that don't belong.

Collapse
 
dsaga profile image
Dusan Petkovic • Edited

in essence you have one EventProvider that you wrap your app with, then have a context that passes methods for triggering, subscribing, unsubscribing, and have a custom hook for it called
useEvent();

const { subscribe, trigger } = useEvent();


trigger("post:published",arg1,arg2); 


useEffect(() => {

const handler = (arg1,arg2) => {
  console.log("do something here");
}

subscribe("post:published",handler);
return () => unsubscribe("post:published",handler)

},[subscribe])

Enter fullscreen mode Exit fullscreen mode
Collapse
 
jrock2004 profile image
John Costanzo

A thing to consider here is if someone has your app opened in multiple tabs. I could be mistaken but that window event would reach both tabs

Collapse
 
nicolalc profile image
Nicola

Thanks John, indeed I need to check that,I would prefer to keep tabs independent from each others but I need to check how does this works in that case. Anyway, the solution to that should be simple by just triggering the events within the document context instead of the window, but smth to look at

Collapse
 
chandragie profile image
chandragie

But, isn't that a good thing? 🤔 If I open a same page in two tabs (purposely or not), then if I clicked on some action (say, add to cart), I would also want the cart being updated in the idle tab. Am I imagining it wrong?

Collapse
 
am4nn profile image
Aman Arya

Not exactly. The problem arises when actions from both tabs start interfering with each other. For example, if you're editing a document in two tabs and save different changes in both, merging them could lead to conflicts or data loss. It’s better to keep actions isolated per tab to maintain consistency and avoid unexpected behavior.

Collapse
 
kc900201 profile image
KC • Edited

If it comes to the state management side, wouldn't using useContext be a more effective approach to solve the decoupling parent and children?

Collapse
 
nicolalc profile image
Nicola

Thanks for you comment, the event system is not intended as a state management replacement system, but as a communication between components alternative

Collapse
 
danlee1996 profile image
Daniel Lee

Interesting post.

Please expand more on the pros and cons of this approach compared to using a global state library!

Collapse
 
nicolalc profile image
Nicola

Thanks Daniel! But this is not intended as a global state management system, but as a communication alternative to callbacks

Collapse
 
coderg01 profile image
Harsh

Bro the font family is not readable.

Collapse
 
nicolalc profile image
Nicola

Thanks Harsh, you mean the graphs font?