React Data Binding using a Simple Messaging API
Sometimes props don't cut it. Open possibilities in your React App with a simple messaging API to bind data among components.
Follow along with the ready-to-go git project, React Data Binding
Introduction
React's principle (and somewhat revolutionary) feature is the use of props. By making props a first-class citizen React can detect state changes and do some magic with the Virtual Dom to re-render only those components affected by that state change. See Virtual DOM and Internals to start down that rabbit hole.
If all you do is pass props up the tree, aka parent -> child, then this makes a lot of sense. But things get a little more nuanced in the following scenarios:
- Child updating the parent
- Updating a component in a completely separate branch of the hierarchy
- Updating a component that's not even in the hierarchy
- Updating a component in a list (ugh... lists)
Each of these can be solved using React features. I'm not going to go into these since that's a few blog posts in and of itself. What I can do is cover how to solve all of them using data binding with a simple messaging API.
Design Principles
There are a good many ways to implement a messaging API, and all are well-established patterns used in programming. The two most commonly referenced are the:
The observer pattern is not well suited for our needs, so we'll focus instead on the Publish / Subscribe (pub/sub) pattern which very much is.
Data binding is just a general programming term used to describe data that is synchronized among multiple interests. Angular is often referred to as a true data binding framework, and is a huge consideration when deciding between Angular and React. See Angular Data Binding. There are a lot of opinions on this in React, which out of the box does not support data binding. See Data Binding in React SO for what I mean. I am simply providing my own opinion. It's worked well, for even fairly complex applications.
Pub/Sub is built using 3 components:
- Publisher - Creates and sends (publishes) a message.
- Broker - Handles all published messages and broadcasts them to subscribers
- Subscriber - Listens for (subscribes to) messages and maybe does something based on the message
Here's a simple diagram of the flow
What this ends up looking like in code is that both publishers and subscribers need to know about the broker. Publishers send messages to the broker and subscribers tell the broker what they are subscribing to. Publishers and subscribers, on the other hand, do not need to know about each other. This is what makes this pattern so powerful. The broker holds collections of subscribers and messages, and as subscribers pass messages to the broker the broker passes those messages on to the subscribers. Simple enough.
To demonstrate the power of this seemingly simple pattern, consider a bullet hitting an enemy in a game. When that bullet hits the enemy: The enemy takes damage, a blood spurt image is displayed, a sound is played of the bullet hitting the enemy, the enemy yells, the player also yells, all the other NPEs yell, the other enemies yell, the enemy possibly dies, and if the enemy dies the player gets a point, and on and on and on. Using this pattern, a single piece of code can send out a message that says, "a bullet hit something" and everyone subscribed to that event can decide if it should do something. As a programmer, you can add and subtract subscribers with ease and never have to refactor your existing code!
There are two things a subscriber and publisher do need to share an understanding of. The type of message and what the message is.
Very often the type is simply a String, like mousedown
. I've also seen Enums (much preferred) and integer codes like 46 (very annoying). The important thing here is that the type can be used to identify the message so the broker knows who to send the message to and subscribers know what they are getting.
The message itself is less clear. It could be anything, assuming the language supports it. In the case of a mousedown
event in Javascript, the Message is a MouseEvent object. It could also be a reference to an object or nothing at all.
The Approach
I mentioned the mousedown
event for good reason. It's part of a built-in API all web browsers know (or better know) called the UI Events API. This API is built over an even more fundamental pub/sub system. The DOM publishes all sorts of events and so long as you register a listener by calling addEventListener
for an event, you'll hear about it.
You can create custom events leveraging that pub/sub system. We don't have to invent anything here, which is great because implementing a broker can get tricky. All we need to do is create some custom events and manage our publishers and subscribers.
Getting this to work in React does come with some gotchas, which I'll cover. But all we're going to be doing here is leveraging what is already provided for us. See Custom Events for where I referenced the API we're using.
Implementation
This implementation has 4 parts
- The EventBus, whose job is to create, send, and remove listeners.
- The Event Enum. Optional, but I prefer it.
- Adding send events in a React component.
- Adding event listeners in a React component.
The EventBus
This is just a piece of code that wraps up the management of Custom Events so we don't have to duplicate it all over the place. I'll just give you my version:
EventBus.js
const eventBus = {
on(event, callback) {
document.addEventListener(event.getName(), callback)
},
send(event, data) {
document.dispatchEvent(new CustomEvent(event.getName(), { detail: data }))
},
remove(event, callback) {
document.removeEventListener(event.getName(), callback)
},
}
export default eventBus
It does 3 things. It adds listeners for events using the on
function, it sends messages using the 'send' function, and it removes listeners using the remove
function.
A note on CustomEvents. The use of
{ detail: data }
is not optional. This is the only way to pass data. Attempting to send a message in any other format will result in a lot of null errors.
The Event
Javascript doesn't have native enums, but you can implement it. A quick search found this Enums, and it more or less covers your options. I like the class approach because it models how enums work in otehr languages I use.
Event.js
export default class Event {
static GLOBAL_MESSAGE = new Event('GLOBAL_MESSAGE')
#name
constructor(name) {
this.#name = name
}
}
This just makes it easier to share an understanding of what messages are available. You could simply use a String and be done with it, but when you have a bunch of listeners out there it can get frustrating to debug issues when you mistyped something.
Sending Events
Ok, now we want to incorporate this into a React component. Our first component sends a message.
SendGlobalMessage.js
import React from 'react'
import { Event, EventBus } from 'event'
export default function GlobalFunction() {
const [message, setMessage] = React.useState('')
function handleChange(event) {
setMessage(event.target.value)
}
function sendMessage() {
EventBus.send(Event.GLOBAL_MESSAGE, message)
}
return (
<div>
<input value={message} onChange={handleChange} />
<div onClick={sendMessage}>Send</div>
</div>
)
}
This simple little component takes some input and stores it in state, and when the Send
button is pushed broadcasts that input out to anyone that cares. No one is listening right now, so nothing will happen.
I just want to point out that my imports for Event and EventBus are placeholders for whatever you're own imports might look like
Adding Listeners
Now we want to add some listeners for that GLOBAL_MESSAGE type. Here's how that looks.
DisplayGlobalMessage.js
import React from 'react'
import { Event, EventBus } from 'event'
export default function GlobalFunction() {
const [message, setMessage] = React.useState(null)
const handleGlobalMessage = React.useCallback((data) => {
setMessage(data.detail)
})
React.useEffect(() => {
EventBus.on(Event.GLOBAL_MESSAGE, handleGlobalMessage)
return () => EventBus.remove(Event.GLOBAL_MESSAGE, handleGlobalMessage)
})
return (
<div>
<div>Global Message</div>
<div>{message}</div>
</div>
)
}
Let's break this down. The first piece of code is really important, I'll explain why below. This is just the function that the broker calls when it tries to send the message. All it does is set some state so it can be rendered in the UI.
const handleGlobalMessage = React.useCallback((data) => {
setMessage(data.detail)
})
The second piece of code adds the listener and also removes the listener when the component is unmounted. So we're telling the broker that anytime a GLOBAL_MESSAGE message is sent please call this handleGlobalMessage function.
React.useEffect(() => {
EventBus.on(Event.GLOBAL_MESSAGE, handleGlobalMessage)
return () => EventBus.remove(Event.GLOBAL_MESSAGE, handleGlobalMessage)
})
Calling React.useEffect
without a second argument is really just telling React to call the function when the component mounts, but don't bother ever calling it again. Perfect for adding listeners. When your Effect function returns something, React calls it when the component un-mounts. Perfect for removing listeners. If you need a primer on this, especially if you are used to React Classes, take a look at React Use Effect.
Removing Listeners
The reason the useCallback
piece is so important is that if you do not wrap handleGlobalMessage in useCallback
, your listener will never get removed from the DOM. Let me explain. The Event API relies on function identification in order to remove it, and functions are identified by reference. See EventTarget for more. All functions that are included in a functional React component are rebuilt every time your component is called by React. I should mention here that your component being called is not the same as your component being re-rendered. Anyway, this means the reference to every function is also changed. By the time you call remove on your event listener, React has very likely destroyed and created your function multiple times.
To provide a way to maintain the reference to a function across component rebuilds, React gives us React.useCallback
. UseCallback. So now we'll be removing the same reference to handleGlobalMessage
that we added.
Removing a listener is very, very, very important. If you do not remove listeners the DOM will accumulate orphaned listeners, and this will either cause your page to run out of memory, start throwing thousands of errors, or cause your page to become slow to unresponsive because you have thousands of listeners consuming CPU.
Applying to Use Cases
Going back to our original problems:
- Child updating the parent
- Updating a component in a completely separate branch of the hierarchy
- Updating a component that's not even in the hierarchy
- Updating a component in a list
There's really no need to go into specifics on how to implement this for the first three cases. Since the component listening for a message can be anywhere, it's really just a matter of setting up the message.
Lists can be a little more nuanced. Dealing with lists in React could be its own blog post, so I'll just make a few quick mentions. This is not always the right way to make state change in lists. It really depends on what you're trying to do. It can also cause fun bugs if you're not careful. Trying to change state directly in the element then also trying to change state of the list itself can cause one such bug. Remember, you might change the state of the element, but if the list being stored in state is not updated a re-render will blow away the elements changes. There's also the danger of adding a lot of listeners to the DOM depending on how large your list is. If you have a list with a thousand elements, maybe don't add a listener to each one. There are ways of targeting elements that need listeners, but again this depends on specifics.
Considerations
Performance
When I started playing around with this design this the first question I asked was, "how expensive are listeners?". The answer is, it depends. For context, here's a view of Amazon's home page and the list of registered listeners. I expanded one event type. Everything listed will have n number of listeners. So there are a lot.
What we don't know is what each listener is doing, and I think this is the most important consideration. This SO question more or less devolves to this point. You could add thousands of click events and if each does nothing but log a "click!", it's unlikely to affect performance. But if each listener tried to calculate the first 1 million prime numbers, I suspect we'd have a performance issue on our hands.
In React the general pattern here is to simply set new state when a listener receives a message. This in turn will cause that component to re-render. This could cause performance issues, but I have never seen this. Presumably because React is very good at re-rendering components.
Async
Listeners are running as unblocking async tasks, like just about everything else in modern Javascript engines. This means that data binding in this implementation is not immediate. This is often not a problem, but it is something to consider.
Multiple Sources of True
If you have a component that is accepting data from multiple places, beware. Generally speaking, maintaining a source of truth for data is always a good idea. With good design, introducing data binding to a system can enforce sources of truth. Bad design, on the other hand, can cause all sorts of issues.
For example, let's say you have some data in a database. The database is the obvious source of truth. But you don't want to be hitting the database all the time so you put a caching layer between the user and the database. With good design the caching layer can become the source of truth, leverage the messaging system, and let components know that something has changed. Mutations will never get missed. On the other hand, with bad design, mutations to data are stored as state in multiple places. As React re-renders components you never really know what your going to get. You update a component with a message but forget to update state that originally populated that component, on re-render that component is now reflecting out of date changes.
There's a lot to unpack in this example, but my point is that you need to be cautious of who your sending messages to, and definitely who you are not sending messages to.
Scope
You may have noticed that the code example for adding messages uses document
and not window
. If you go back to that screen shot of the Amazon listeners, you'll notice some are attached to the Window
, some are attached to the document
, and others are actually attached to various elements. I didn't complicate my api by introducing where listeners are being attached, but document
is a pretty safe bet. This too is out of scope for this blog, but it is something to be aware of.
Conclusion
I covered the basics of using a Pub/Sub messaging system to implement data binding in a React app. I've found this very useful as React apps start getting complex and I need to start refactoring. When you start moving components, you also need to ensure you didn't muck up the props. With data binding you don't necessarily take props out of the equation, but you can disentangle components allowing for greater flexibility.
This pattern also becomes very powerful when paired with the use of the MVC (model, view, controller) pattern. This is out of scope of this blog but also isn't novel. It's a driving principle in Angular's architecture, and something I allude to in the Multiple Sources of True consideration. This is typically where I employ this pattern, especially when dealing with complex user interactions tied to databases and local caches. You want a source of truth for data, and this pattern can help.
But how this pattern can be used is wide open. I'd be interested to hear how others may find it useful.
Reference a simple starter project that includes the code used in this blog by going to React Data Binding. And thanks for reading!