Handling Communication Between Non-Related Components in ReactJS

In React applications, there are scenarios where components need to communicate or share data, but they do not have a direct parent-child relationship. This situation is common in complex applications where components may be deeply nested or situated in different parts of the component tree. To address this, we can use event systems to facilitate communication between such components. These event systems typically involve subscribing to and dispatching events.

Key Concepts of Event Systems

The basic operations of any event system are:

  • Subscribe/Listen: A component subscribes to an event to be notified when that event occurs.
  • Send/Trigger/Publish/Dispatch: A component sends or dispatches an event to notify subscribers.

There are three main patterns for implementing these event systems:

  1. Event Emitter/Target/Dispatcher
  2. Publish/Subscribe (Pub/Sub)
  3. Signals

Pattern 1: Event Emitter/Target/Dispatcher

In this pattern, the listeners need a reference to the source to subscribe to an event. This pattern is commonly seen in traditional JavaScript with the EventTarget interface.

Example:

Event Emitter Implementation:

import React, { useEffect } from 'react';

class EventEmitter {
constructor() {
this.events = {};
}

addEventListener(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}

removeEventListener(event, listenerToRemove) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove);
}

dispatchEvent(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(data));
}
}

const eventEmitter = new EventEmitter();

function ComponentA() {
useEffect(() => {
eventEmitter.addEventListener('myEvent', handleEvent);

return () => {
eventEmitter.removeEventListener('myEvent', handleEvent);
};
}, []);

const handleEvent = (data) => {
alert(`Event received with data: ${data}`);
};

return

Component A

;
}

function ComponentB() {
const triggerEvent = () => {
eventEmitter.dispatchEvent('myEvent', 'Hello from Component B');
};

return (

);
}

function App() {
return (


);
}

export default App;

Pattern 2: Publish/Subscribe (Pub/Sub)

In the Pub/Sub pattern, subscribers do not need a direct reference to the event source. Instead, a global object handles all the events, making it accessible throughout the application.

Example:

Pub/Sub Implementation:

import React, { useEffect } from 'react';

class PubSub {
constructor() {
this.events = {};
}

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

unsubscribe(event, callbackToRemove) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(callback => callback !== callbackToRemove);
}

publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(data));
}
}

const globalBroadcaster = new PubSub();

function ComponentA() {
useEffect(() => {
const handleEvent = (data) => {
alert(`Event received with data: ${data}`);
};
globalBroadcaster.subscribe('myEvent', handleEvent);

return () => {
globalBroadcaster.unsubscribe('myEvent', handleEvent);
};
}, []);

return

Component A

;
}

function ComponentB() {
const triggerEvent = () => {
globalBroadcaster.publish('myEvent', 'Hello from Component B');
};

return (

);
}

function App() {
return (


);
}

export default App;

Pattern 3: Signals

The Signals pattern is similar to the Event Emitter pattern but avoids using arbitrary strings for event names. Instead, each object that can emit events has specific properties for those events, providing a clear contract for the events it can emit.

Example:

Signals Implementation:

import React, { useEffect } from 'react';
import Signal from 'signals';

class SignalEmitter {
constructor() {
this.myEvent = new Signal();
}
}

const signalEmitter = new SignalEmitter();

function ComponentA() {
useEffect(() => {
const handleEvent = (data) => {
alert(`Event received with data: ${data}`);
};
signalEmitter.myEvent.add(handleEvent);

return () => {
signalEmitter.myEvent.remove(handleEvent);
};
}, []);

return

Component A

;
}

function ComponentB() {
const triggerEvent = () => {
signalEmitter.myEvent.dispatch('Hello from Component B');
};

return (

);
}

function App() {
return (


);
}

export default App;

Each pattern has its advantages and use cases. Event Emitters are straightforward for direct component communication, Pub/Sub is ideal for decoupling event sources and listeners, and Signals provide a structured approach with explicit event properties. By understanding and utilizing these patterns, developers can enhance the flexibility and maintainability of their React applications.