CodewCaro.
If you're knee-deep in a complex Multi-Page Application (MAP) using React, components often need to communicate with each other to share data or trigger actions. While React's prop-passing and context API provide solutions for component communication, they sometimes introduce unnecessary complexity, especially for deeply nested components or those across different component trees. I found the EventBus pattern useful for simplifying state updates. By using Event Bus you can save you and your team a whole lot of headache.
An event bus is a design pattern used in software development to enable objects or components to interact with each other without being directly connected. It acts as a central communication channel where components can send messages (events) and listen for events, creating a publish-subscribe (pub-sub) model of communication. This concept can be applied both in frontend aswell as backend.
When it comes to frontend development we sometimes need to throw around states in our application. This could be an application handling shopping carts, planning tools or a media gallery.
React offers a wide range of tools. But solutions like Redux and prop passing can increase the complexity of the application and can be hard to maintain. Imagine a Redux application that introduces a whole lot of it's own concepts, such as reducers, actions and stores. Debugging this Redux application can take a lot of time and most often you need to trace where states were pushed very closely in order to fix bugs.
What's the point of EventBus? Components don't need to be aware of each other, decoupling reduces dependencies and increases modularity. It simplifies the logic since you don't need to pass a 100 props between eachother. Lastly it enables for communication between components. It doesn't have to be a parent to child pattern. You can have a more flat structure of communcation.
import React, { createContext, useRef, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
export const GlobalEventBusContext = createContext();
const GlobalEventBusContextProvider = ({ children }) => {
const eventObservers = useRef([]);
const registerEventListener = useCallback((listener) => {
const index = eventObservers.current.findIndex(
(e) => e.event === listener.event && listener.source === e.source
);
if (index !== -1) {
eventObservers.current[index] = listener;
} else {
eventObservers.current.push(listener);
}
}, []);
const notifyEventListeners = useCallback((event) => {
eventObservers.current.forEach((listener) => {
if (listener.event === event.type) {
listener.exec(event.value);
}
});
}, []);
const contextValue = useMemo(() => ({
notifyEventListeners,
registerEventListener,
}), [notifyEventListeners, registerEventListener]);
return (
<GlobalEventBusContext.Provider value={contextValue}>
{children}
</GlobalEventBusContext.Provider>
);
};
GlobalEventBusContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default GlobalEventBusContextProvider;
First, make sure the GlobalEventBusContextProvider is properly set up in your application. This is usually done near the root of your application, for example in your App component.
import React from 'react';
import GlobalEventBusContextProvider from './GlobalEventBusContextProvider';
import MediaGallery from './MediaGallery';
import UploadAssetModal from './UploadAssetModal';
function App() {
return (
<GlobalEventBusContextProvider>
<div>
<h1>Asset Management Application</h1>
<UploadAssetModal />
<MediaGallery />
</div>
</GlobalEventBusContextProvider>
);
}
export default App;
Time to send updates of state to the GlobalEventBusContext from your component doing the API requests. Maybe you have a upload or something.
import React, { useContext } from 'react';
import { GlobalEventBusContext } from './GlobalEventBusContextProvider';
function UploadAssetModal() {
const { notifyEventListeners } = useContext(GlobalEventBusContext);
const uploadAsset = () => {
const createAssetResults = "Asset created successfully";
// sends create state update to other components
notifyEventListeners({ type: 'create', value: createAssetResults });
};
return (
<div>
<button onClick={uploadAsset}>Upload Asset</button>
</div>
);
}
export default UploadAssetModal;
We also need a subscriber component for updating the rendering (A Media Gallery in this example).
import React, { useContext, useEffect, useState } from 'react';
import { GlobalEventBusContext } from './GlobalEventBusContextProvider';
function MediaGallery() {
const [selectedAssets, setSelectedAssets] = useState([]);
const { registerEventListener } = useContext(GlobalEventBusContext);
useEffect(() => {
const refresh = () => {
setSelectedAssets([]);
window.location.reload(false) //do a hard refresh
console.log("Assets refreshed");
};
//registering multiple actions to a single refresh function
['create', 'publish', 'unpublish', 'update', 'replace', 'rename', 'delete'].forEach((action) => {
registerEventListener({
event: action,
exec: refresh,
});
});
}, [registerEventListener]);
return (
<div>
<h2>Media Gallery</h2>
</img source={selectedAssets.img}>
</div>
);
}
export default MediaGallery;
Yes, in order to prevent rerenders we did some checks to check for new updates.
useCallback returns a memoized version of the callback function that only changes if one of the dependencies has changed. In this case, the dependencies array is empty [], meaning the registerEventListener function will not be recreated between re-renders unless the component itself is re-created. This is to prevent rerenders.
registerEventListener is to add a new listener to a list of event listeners, which is stored in a ref called eventObservers. This ref only lives for the lifetime of the component, avoiding the loss of registered listeners between re-renders.
The function looks for an already registered listener in eventObservers.current that matches the event and source of the new listener. It uses findIndex to find the position of this listener in the array. If it finds a match, findIndex returns the position (index) of that listener. If no match is found, it returns -1.
If a matching listener is found (meaning the index is not -1), it's replaced with the new one. This avoids having multiple listeners for the same event. If no matching listener is found (the index is -1), the new listener is added to the list.