Event Driven Architecture has been around for decades. This is not something new that we discovered recently. But we have definitely seen the rise of this with the emergence of distributed system and need for asynchronous communication.
Event Driven Architecture really helps to reduce the complexity involved with distributed system while providing a way to asynchronously communicate with each other. This helps in making the application more modular (since there is no coupling involved), more reactive and easy to scale.
When I tried explaining the EDA using Event Bus to my colleague, I got a question, asking “How is it different than redux state management?”. Since in redux one component updates the state and then the other component listen to the state and updates itself. Well, these two are completely different mechanism. Redux is a centralized state management system whilst Event Bus is a communication mechanism.
Table of Contents
How Event Bus Different From Redux State Management System
You might be right in thinking that both Event Bus and Redux are doing the similar thing – they are both managing state of the of the application. But you might be overlooking the fact that they are serving two completely different purpose.
I get the confusion though, since Redux and Event Bus both are based on the Event Driven Architecture. The subtleties are in the purpose for which they are built for.
The Event Bus is a way for different components of an application to communicate with each other through events, while Redux is a state management tool that provides a centralized store for an application’s state.
In an event-driven system, each component can publish events that other components can subscribe to, and when an event is published, all subscribed components are notified and can react accordingly. This makes the application more modular and easier to maintain, as components are decoupled from each other and can react to events independently. On the other hand, Redux provides a centralized store for an application’s state, making it easier to manage and access the state across different components. While both event-driven communication and Redux can be used in the same application, they serve different purposes and can be used to solve different problems.
Immediate Benefits Of Using Separate Event System For Communication
There are many benefits to separating out the Event System from state management. Some of them are as follows:
You don’t always need state to change based on an event
Whilst it is common for state to be changed in response to an event you don’t always need to connect state change with an event. Sometimes all that is needed is to start an asynchronous background process and have it report when it is done. Bringing external state in where it is not required leads to added complexity and overhead.
You have limited your dependency exposure to a small simple component
An eventing system will become a major dependency hazard for any large app as every component that has to communicate with another will need to have access to an instance of your event dispatcher. By using a separate Event System we have avoided automatically connected state management to all your app components.
You can manage asynchronicity however you like
Events are no longer tied to state change so async becomes easier. Want to use an async function to manage asynchronous events? Easy. You control how asynchronicity is managed and what state dependencies you have available.
For a detailed view, I recommend reading following article:
How Event Driven Communication Different than a Direct Method Call?
First of all Event Driven communication is asynchronous. That means it doesn’t wait for other components to return or do something. It fires and forget kinda of mechanism. Another thing is that it doesn’t know about the other component who might be using the events raised by this component. Thus resulting in a structure that is completely decoupled from one another and hence highly modular and manageable.
Whereas direct method call means that one component invokes the method of the other component. Therefore, component one knows about the other component and this creates a tight coupling.
In event driven components creates events that is published to the central event system or event bus, then event bus pass the events to all its subscribers. This is how it will look like in a diagram:
If you have been working in frontend, you are used to working with events. Events such as onPageLoad
, onClick
, onChange
etc. You consume these events and provide certain functionalities to it. It is similar to that. This makes your application reactive and allows for seamless and responsive user experience where changes in data and user input are reflected in near real time.
Implement Event Driven Communication Using Custom Event Bus (React/Typescript)
When I faced the situation to implement this architecture, I directly started by writing a small lightweight event bus. Because I knew the concept beforehand 😀
And I think that is the best way to learn and understand how event bus works.
So, event bus simply follows the pattern of Publisher/Subscriber.
It means that there will be two group of users,
- Publishers
- Subscribers
Publishers are the ones who will be creating events. These events will then be published to the event bus.
Subscribers are the receivers of the events. The events published by the publishers are received by the receivers on the other side of the event bus. And Event Bus acts as the medium to transport these messages from publishers to the subscribers.
With that said, let’s implement a very simple light weight event bus.
export interface Subscriber {
name: string
callback: (data: any) => void
}
class EventBus {
private static instance: EventBus | undefined
private subscribers: Map<string, Array<Subscriber>>
constructor() {
this.subscribers = new Map();
}
public static getInstance() {
if (EventBus.instance === undefined) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
public async publish(topicName: string, data: any) {
let subscribers = this.subscribers.get(topicName);
console.log(subscribers);
subscribers!.forEach(s => s.callback(data));
}
public async subscribe(topicName: string, subscriber: Subscriber) {
if (!this.subscribers.has(topicName)) {
this.subscribers.set(topicName, []);
}
let subscriberList = this.subscribers.get(topicName);
subscriberList!.push(subscriber);
}
public async remove(topicName: string, subscriberName: string) {
let subscriberList = this.subscribers.get(topicName)!;
this.subscribers.set(topicName, subscriberList.filter(s => s.name !== subscriberName))
}
}
export default EventBus;
Woah, I pasted everything at once 😀
Let’s go through each section one-by-one.
Create a Singleton for Event Bus
Creating a singleton class for event bus will ensure that there is only one instance of EventBus
at all time. And that instance will be shared across all the component in the application. This way you can make sure that all components are using the same event bus to receive the message.
class EventBus {
private static instance: EventBus | undefined
private subscribers: Map<string, Array<Subscriber>>
constructor() {
this.subscribers = new Map();
}
public static getInstance() {
if (EventBus.instance === undefined) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
.
.
.
Subscribe To Topics
The following piece of code takes in the topic name and subscriber info to add it to a topic’s list of subscribers. Once the subscriber invokes this method, their information is persisted in memory.
private subscribers: Map<string, Array<Subscriber>>
.
.
.
public async subscribe(topicName: string, subscriber: Subscriber) {
if (!this.subscribers.has(topicName)) {
this.subscribers.set(topicName, []);
}
let subscriberList = this.subscribers.get(topicName);
subscriberList!.push(subscriber);
}
Publish Events to Topics
When the publisher calls this method, they pass the topic
name (where they want to publish message) along with the data
. Then this method is responsible to get all the subscribers for the given topic and pass the data
to them.
The following code is responsible for that:
private subscribers: Map<string, Array<Subscriber>>
.
.
.
public async publish(topicName: string, data: any) {
let subscribers = this.subscribers.get(topicName);
console.log(subscribers);
subscribers!.forEach(s => s.callback(data));
}
Remove Subscribers
When the page is unmounted then we need a way for the components to un-subscribe themselves. This is needed because when they are not active, it is not useful for them to receive the message.
Also, it could create some side-effects and unwanted behaviour. So, its better to unsubscribe when the component un-mounts.
And for that we need to provide them with the way to let event bus know that they are no longer the subscribers.
The following code un-subscribers the given subscriber from the list by their name:
public async remove(topicName: string, subscriberName: string) {
let subscriberList = this.subscribers.get(topicName)!;
this.subscribers.set(topicName, subscriberList.filter(s => s.name !== subscriberName))
}
There is another way of using the event bus which is provided in all modern browsers. That is called the Broadcast Channel API.
Implement Event Driven Communication Using Broadcast Channel (React/Typescript)
Now-a-days there is an inbuilt BroadcastChannel
API that is offered by every modern browser. This API is quite powerful, unlike the Custom Event Bus that we created the above example, the Broadcast Channel API
allows different tabs or windows of the same origin to communicate with each other through messages.
It is extremely simple to use as well. All you have to do is create an instance of the BroadcastChannel and pass the topic name to it. Then you can use it in your application. You can create multiple instances of the BroadcastChannel (it need not be singleton) and then use them to communicate over the channel.
Here’s the sample example on how to use Broadcast API.
// Connection to a broadcast channel
const bc = new BroadcastChannel("test_channel");
// Example of sending of a very simple message
bc.postMessage("This is a test message.");
// A handler that only logs the event to the console:
bc.onmessage = (event) => {
console.log(event);
};
// Disconnect the channel
bc.close();
In order to replace use Broadcast Channel instead of custom event bus, simply replace the subscriber code as following:
useEffect(() => {
const channel = new BroadcastChannel("channel:topic");
channel.onmessage = (event: any) => {
console.log("Event Data: ", event);
if (event.data !== null)
// update state or do something with the data
}
}, [])
And to post the message, do the following:
const Component: React.FC = () => {
const channel = useMemo(() => new BroadcastChannel("channel:topic"), []);
const onSomeChangeEvent = (e) => {
channel.postMessage(e.target.value);
}
.
.
.
}
It is that simple.
Just be caution because this will work across tabs and browser windows on same origin. So, just make sure you want this functionality in your application.
Event Bus in Action
I created a following page to demonstrate the use of Event Driven Communication between components. I’ve taken an example of a demo Machine Health Viewer where there are multiple assets that needs to be filtered on the basis of some location.
In the page below, there are two components:
- Asset Filter
- Asset List View
As soon as we change the asset filter, an event is published to the event bus on FILTER
topic. This event is then relayed to the subscriber (Asset List Viewer) by the event bus. And then Asset List Viewer component updates the list accordingly.
On changing the filter, Asset List View is changed as well. This happens completely asynchronously and de-coupled way. Here, Asset Filter component doesn’t even know that Asset List View exists and vice-versa. They are simply reacting to the events they receive without know who sends the events.
Here’s the entire code for the above demo. Make sure to copy the code correctly. Also, I replaced my custom event bus with the PostalJS. If you want to use the above event bus then update the code accordingly.
Conclusion
In conclusion, event-driven communication is an important concept in frontend development, particularly in the context of building scalable and reactive web applications.
With event-driven communication, different components of an application can communicate with each other through events, simplifying communication and making the application more modular and easier to maintain.
In this blog post, we discussed the concept of event-driven communication and its benefits, and illustrated how it can be implemented in React TypeScript using a simple use-case of Filter and View. I hope that this blog post has provided you with a better understanding of event-driven communication and its importance in frontend development, and encourages you to consider using it in your own projects.
I’m always reading my comments, so feel free to drop by if you have any queries or suggestions.