It is never to late to start simplifying your dev life. Start using the BroadcasterJS pub/sub event bus today!
Install broadcasterjs (or copy code)
npm i @foundit/broadcasterjsImport broadcaster.ts
import { broadcast } from '@foundit/broadcasterjs'Subscribe:
const off = broadcast.on(['EXAMPLE-FLAG', ({detail: myData}) => setMyUseState(myData)])Publish:
broadcast.emit('EXAMPLE-FLAG')Publish with some payload data:
broadcast.emit('EXAMPLE-FLAG', myData)Unsubscribe (execute the subscribe return function):
off()BroadcasterJS is a pub/sub event transmitter written in typescript. A subscriber in one part of your app is always ready to execute a function triggered from another part of your app. With or without arguments. Use it once or architect your whole web app infrastructure around events. Either way this lightweight gem of code will suit your needs.
BroadcasterJS is framework agnostic and therefor doesn't in itself trigger any rerenders. So to get it back into the React realm just put the subscriber in a useEffect like so. Emitters can be trigger anywhere.
In React wrap the subscriber in a useEffect. Return it if the listener should unsubscribe on component unmount. Omit the return if the listener should persist:
useEffect(() => {
return broadcast.on(['EXAMPLE-FLAG', () => setMyUseState(true)]);
}, [])Only one of each subscribers (flag + listener combination) is set. A rerender will not add the same subscriber again.
Setting the flag in upper case is just a best practice which makes for better code readability/maintainability.
A click invokes a broadcast (outside of React) that is received in the sibling component where it sets a local state back in React that then triggers a re-render.
Emitter
Receiver
No props where abused in this button click. Scouts honor.
broadcaster.js | broadcaster.ts
export type ListenerProps = <T extends unknown>([type, listener, settings]: [
type: string,
listener?: T,
settings?: settingsType
]) => string | void
export interface returnType {
on: ListenerProps
once: ListenerProps
off: ListenerProps
emit: (type: string, detail?: unknown) => boolean
}
export interface settingsType {
debug: boolean
debugGlobal: boolean
allowDoublettesSubscribers: boolean
}
let broadcastItemsCache: string[] = []
let globalDebug =
new URLSearchParams(window.location.search).get('debug')?.toLowerCase() ===
'broadcasterjs'
const defaultSettings = {
debug: false,
debugGlobal: false,
allowDoublettesSubscribers: false,
}
const eventBus = (): returnType => {
const hubId = ' broadcast-node '
const on = <T extends unknown>([type, listener, settings = defaultSettings]: [
type: string,
listener?: T,
settings?: settingsType
]): string => {
const options = setOptions(settings)
const { exists, id } = handleCache().listenerExists(type, listener, options)
if (exists && !options.allowDoublettesSubscribers) return id
if (options.debug)
debugmode({
string: `Setting listener for "${type}"`,
obj: listener,
force: true,
})
const eventTarget = createOrGetCustomEventNode(hubId)
eventTarget.addEventListener(
'broadcast-' + type,
listener as EventListenerOrEventListenerObject
)
return id
}
const once = <T extends unknown>([
type,
listener,
settings = defaultSettings,
]: [type: string, listener?: T, settings?: settingsType]) => {
const options = setOptions(settings)
const { exists, id } = handleCache().listenerExists(type, listener, options)
if (exists && !options.allowDoublettesSubscribers) return id
if (options.debug)
debugmode({
string: `Setting "once" listener "${type}"`,
obj: listener,
force: true,
})
const eventTarget = createOrGetCustomEventNode(hubId)
eventTarget.addEventListener(
'broadcast-' + type,
listener as EventListenerOrEventListenerObject,
{ once: true }
)
return id
}
const off = <T extends unknown>([
type,
listener,
settings = defaultSettings,
]: [type: string, listener?: T, settings?: settingsType]) => {
const options = setOptions(settings)
if (options.debug)
debugmode({
string: `Removing listener "${type}"`,
obj: listener,
force: true,
})
handleCache().remove(type, listener)
const eventTarget = createOrGetCustomEventNode(hubId)
eventTarget.removeEventListener(
'broadcast-' + type,
listener as EventListenerOrEventListenerObject
)
}
const emit = (
type: string,
detail?: unknown,
settings?: settingsType
): boolean => {
debugmode({
string: `Emitted ${type}`,
obj: detail,
force: settings?.debug,
})
const eventTarget = createOrGetCustomEventNode(hubId)
return eventTarget.dispatchEvent(
new CustomEvent('broadcast-' + type, { detail })
)
}
return { on, once, off, emit }
// Initiate or retreive node for custom event.
function createOrGetCustomEventNode(hubId: string): Node {
const nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_COMMENT
)
while (nodeIterator.nextNode()) {
if (nodeIterator.referenceNode.nodeValue === hubId) {
return nodeIterator.referenceNode
}
}
return document.body.appendChild(document.createComment(hubId))
}
// Store each subscription (flag + details object serialized and hashed) in an array
// taking advantage of the es6 modules intrinsic singleton properties.
// If already stored reject request and exit silently.
function handleCache() {
const listenerExists = (
type: string,
listener: unknown,
settings: settingsType
): { exists: boolean; id: string } => {
const id = createBroadcastId(type, listener)
debugmode({
string: 'broadcastItemsCache',
obj: broadcastItemsCache,
force: settings.debug,
})
if (broadcastItemsCache.includes(type + id)) {
debugmode({
string: 'Prevented doublette subscriber.',
force: settings.debug,
})
return { exists: true, id }
}
broadcastItemsCache.push(type + id)
return { exists: false, id }
}
const remove = (type: string, listener: unknown) => {
const removeId = createBroadcastId(type, listener)
broadcastItemsCache = broadcastItemsCache.filter(id => id !== removeId)
}
return { listenerExists, remove }
}
// Serialize+hash the subscriber and store it to not add it twice.
function createBroadcastId(flag: string, details: unknown): string {
let detailsStringified
switch (typeof details) {
case 'function':
detailsStringified = helpers().serializeFn(details as () => void, {})
break
default:
try {
detailsStringified = JSON.stringify(details)
} catch (error) {
throw new Error(
`Could not "JSON.stringify" the broadcasterjs payload of "${typeof details}" type.`
)
}
}
return helpers()
.hashCode(flag + detailsStringified)
.toString()
}
function setOptions(settings: settingsType): settingsType {
const mergedOptions = { ...defaultSettings, ...settings }
if (mergedOptions.debugGlobal) globalDebug = true
return mergedOptions
}
function helpers() {
const hashCode = (s: string) =>
s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)
const serializeFn = (f: () => void, env: unknown) =>
JSON.stringify({ src: f.toString(), env: env })
return { serializeFn, hashCode }
}
function debugmode({
string,
obj,
force,
}: {
string: string
obj?: unknown
force?: boolean
}) {
if (!globalDebug && !force) return
console.log(`%cBroadcast: ${string}`, 'color:#bada55', obj ? obj : '--')
}
}
const broadcast = eventBus()
export { broadcast }
(Even if BroadcasterJS is simple to use the source code for this site is public with plenty of examples that can be scrutinised here: https://github.com/nicatspark/broadcasterjs)
So you implemented it and then things went south. Well, here is how to debug.
You can:

Subscribe with debug locally:
broadcast.on(['EXAMPLE-FLAG', () => {
setMyUseState(true)
}], {debug: true})BroadcasterJS is optimised for a SPA like situation where it can be a tall task to keep track of renders where subscription are set. Since the same custom event easily can be set multiple times BroadcasterJS by default does not allow more than one identical flag + callback combination to be set.
It is easy to circumevent this by sligthly changing the function to include a comment with a number /* 1 */ etc to ensure that the function is unique. You can also disable the guard by including a settings object as a third value in the subscriber array. The subscriber argument array would look something like ['MY-EXAMPLE-FLAG', myCallbackFnToRunOnEvent, {allowDoublettesSubscribers:true}]
Aside from invoking the debug mode through the url with url params you can invoke it throught the settings object. Debug output will then start from when the settings object is set of course so the url method is preferable. Settings object example: ['MY-EXAMPLE-FLAG', myCallbackFnToRunOnEvent, {debugGlobal:true}]