Using Javascript Events to share data between components

Varun Raj
Varun Raj, Co-founder and CTO

As web applications become complex everyday, the data being processed in these apps also become complex. Complex data processing requires an easy way for the data to be shared between components when building a complex web application. At featureOS , we process thousands of data points per hour to help our customers capture customer feedback from their users.

In this post, let us tell you how we use Javascript Events to share data between components with some examples of these Javascript Events.

Take an instance of an app with which has to sync preferences from child to global root, or update theme data from theme picker in a leaf component to parents.

But people could as that we can do this with frameworks like Redux or features like context so why do we need to rely on Events? The major reason is that global state are used for data that are long lived in the application like list of posts, notification list, counters. But for short lived data like configuration change or toggle actions we don’t have to bloat the global state and use events.

Setup

For the purpose of the article, I’ll be setting up a NextJS app and creating the entire setup on top of it.

As a first step we need to create an event bus that manages all the new event dispatch and subscription. So lets create events.js file and create three key functions dispatch, subscribe, unsubscribe

./lib/events.js
export const EventEmitter = {
events: {},
subscribe(event, callback) {},
dispatch(event, data) {},
unsubscribe(event) {}
}

Subscriber

The subscriber function we need to register the provided event handler with the corresponding event. So we need to check if the event is already registered and push the new handler to the list of registered events.

./lib/events.js
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = []
this.events[event].push(callback)
},

Dispatcher

In the dispatch function we need to call all the handlers registered for the given event when the dispatch is called in the application.

./lib/events.js
dispatch(event, data) {
if (!this.events[event]) return
this.events[event].forEach((callback) => callback(data))
},

Unsubscriber

Finally in the unsubscribe function we need to remove the handlers when the application calls it so that the events are not emitted anymore for those events

./lib/events.js
unsubscribe(event) {
if (!this.events[event]) return
delete this.events[event]
}

Now lets put it all together and see how it can be used in our application

./lib/events.js
export const EventEmitter = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = []
this.events[event].push(callback)
},
dispatch(event, data) {
if (!this.events[event]) return
this.events[event].forEach(callback => callback(data))
},
unsubscribe(event) {
if (!this.events[event]) return
delete this.events[event]
}
}

Using Javascript Events in application

We’ll create a simple counter application which will be trigger with help of events. Lets name the events for this case as incrementCounter and decrementCounter

./pages/index.js
import { useState, useEffect } from 'react'
import { EventEmitter } from '../lib/events'
import styles from '../styles/Home.module.css'
export default function Home() {
const [counter, setCounter] = useState(0)
useEffect(() => {
EventEmitter.subscribe('incrementCounter', value =>
setCounter(counter => counter + value)
)
EventEmitter.subscribe('decrementCounter', value =>
setCounter(counter => counter - value)
)
return () => {
EventEmitter.unsubscribe('incrementCounter')
EventEmitter.unsubscribe('decrementCounter')
}
}, [])
return (
<div className={styles.container}>
<h1>Current Value: {counter}</h1>
</div>
)
}

Since we want to try events for sharing data between two different components of different level, let’s create a child component and add the increment and decrement buttons there

Components/CounterButton.js
import React from 'react'
import { EventEmitter } from '../lib/events'
export default function CounterButton({ label, type }) {
const handleClick = () => {
if (type === 'increment') {
EventEmitter.dispatch('incrementCounter', 1)
} else if (type === 'decrement') {
EventEmitter.dispatch('decrementCounter', 1)
}
}
return <button onClick={handleClick}>{label}</button>
}

Now lets import and use it in our main index.js file and see the final result.

./pages/index.js
import { useState, useEffect } from 'react'
import CounterButton from '../Components/CounterButton'
import { EventEmitter } from '../lib/events'
import styles from '../styles/Home.module.css'
export default function Home() {
const [counter, setCounter] = useState(0)
useEffect(() => {
EventEmitter.subscribe('incrementCounter', value =>
setCounter(counter => counter + value)
)
EventEmitter.subscribe('decrementCounter', value =>
setCounter(counter => counter - value)
)
return () => {
EventEmitter.unsubscribe('incrementCounter')
EventEmitter.unsubscribe('decrementCounter')
}
}, [])
return (
<div className={styles.container}>
<h1>Current Value: {counter}</h1>
<CounterButton label='Increment' type='increment' />
<CounterButton label='Decrement' type='decrement' />
</div>
)
}

So with all setup, when we click on the increment or decrement button the counter value changes and if you noticed we didn’t pass any props or have any global state to achieve this. Also with this mechanism it will be easier to create multiple events for minor data changes between different components of the application

The code of this demo can be found here -> Codesandbox