Pub-Sub in a React Application
TL;DR: I implemented the Pub-Sub pattern in a React application. Full sample code can be found here.
I was recently working on a React application and hit a scenario I thought it could be a dead end:
<Parent />
renders<Child />
but both live in separate repositories. This is not a problem per-se and even if they live in the same repository, I would have hit the same scenario.<Child />
is fully autonomous and knows how to fetch data, but doesn't know how to CRUD.<Parent />
knows how to CRUD and needs to tell<Child />
to update itself when something has been created, deleted or updated.
The reason <Child />
doesn’t know how to CRUD is because it can be reused by other parent components where CRUD flows are specific for each one of them and such flows can’t be shared.
The main design decision is that <Child />
should hold the knowledge on how to get data and we can't send it as a prop
via <Parent />
.
Needless to say that fetching data on <Child />
's side is a very expensive and complex process to be replicated everywhere.
For that reason, I’ve decided to implement the Publish-subscribe pattern.
Publish-subscribe Pattern
Publish-subscribe (pub-sub) is a messaging pattern where publishers broadcast messages to its subscribers that are listening to specific events that happen over time.
Think of an e-commerce website with a mini shopping-cart widget in the header: when a customer clicks to buy a product, the shopping cart is automatically updated with the amount of items and price updates so customers have a fast feedback on how much they are going to pay for their purchase.
In some cases, both header and the product page are completely different applications that neither can’t communicate directly with each other nor share the same <Parent />
component. Thus the reason they need some sort of messaging communication to start sharing data back and forth between them.
Another example is a chat application where events (messages) don’t come very often. So we also need to put a listener in place that notifies us when a new message comes in.
This messaging pattern is also widely used by the Observer pattern: a subject keeps a list of dependents (also known as observers) and notifies them of any changes or events. Each observer will then use the streamed data and act accordingly by making API calls, database calls, changes in the UI, etc.
You might also find the above described as Event Bus: an event-driven system that implements the pub-sub pattern.
React Query, one of the most popular data fetching libraries for React (by the time of this writing), uses this pattern in its codebase to notify changes in queries, mutations and cache too.
The Observer class
Now that the overall concept is known, it’s time to code!
My initial approach is to create an Observer
class with tree methods: on
, off
and notify
.
on
: method used to subscribe an observer or, in this case, an event<Child />
is going to be listening to.off
: method to unsubscribe an observer. This is going to be used when an observer is no longer needed or if you think in React, to be used in the cleanup method inside a useEffect hook.notify
: method that is going to broadcast events to all listeners in the app.
type EventObserverType = {
[event: string]: Function[]
}
class EventObserver {
private observers: EventObserverType = {}
on(event: string, observer: Function) {
if (!this.observers[event]) {
this.observers[event] = []
}
this.observers[event].push(observer)
}
off(event: string, observer: Function) {
if (!this.observers[event]) {
return
}
this.observers[event] = this.observers[event].filter(
(ob) => ob !== observer
)
}
notify(event: string, ...args: string[]) {
if (!this.observers[event]) {
return
}
this.observers[event].forEach((observer) => observer(...args))
}
}
Using the Observer
We’ve got two next steps: set up the observer in <Child />
so it can listen to events and notify/trigger those events from <Parent />
:
function Child() {
/**
* subscribes to @ACTION_NAME event to execute "runsSomeComplexCode" method
*/
useEffect(() => {
function runsSomeComplexCode() {
// ...
}
eventObserver.on('@ACTION_NAME', runsSomeComplexCode)
return () => {
eventObserver.off('@ACTION_NAME', runsSomeComplexCode)
}
}, [])
// ...
}
function Parent() {
const sendUpdates = () => eventObserver.notify('@ACTION_NAME')
return (
<div>
<Child />
<button onClick={sendUpdates}>Trigger updates in Child component</button>
</div>
)
}
Conclusion
Propagating props top-down in React is definitely a better approach.
<Parent />
should probably own the data while <Child />
would act as a “dumb” component.
As I have explained at the start of this post I was unable to follow that path and lifting state up could not help me this time. So I had to improvise.
All things considered, I’m pretty happy with this approach and what I initially thought would be a dead end turned out to become a blog post for me to share some of my learnings.
You can find a full example in this codesandbox.
I hope that's helpful and interesting to you. 👋🏼
Did you know you can help me with this page?
If you see something wrong, think this page needs clarification, you found a typo or any other suggestion you might have feel free to open a PR and I will take care of the rest.
My entire site is available to edit on GitHub and all contributions are very welcome 🤙🏼.
Hemerson Carlin, also known as mersocarlin, is passionate and resourceful full-stack Software Engineer with 10+ years of experience focused on agile development, architecture and team building.
This is the space to share the things he likes, a couple of ideas and some of his work.