We use cookies to enhance your experience on the site
CodeWorlds

Event-Driven Architecture in React -- Loose communication

On a spaceship, various systems need to communicate: the engine reports speed changes, shields report damage, the navigation system updates the course. But these systems don't know each other -- they communicate through a central event system. This is exactly Event-Driven Architecture -- a pattern where components communicate through events instead of direct dependencies.

What is Event-Driven Architecture?

Event-Driven Architecture (EDA) is based on:

  1. Emitting events -- a component announces that something happened
  2. Listening for events -- other components react to those announcements
  3. Loose coupling -- the sender doesn't know who is listening; the receiver doesn't know who is emitting

EventBus -- central event bus

1// Simple EventBus with typed events
2class EventBus {
3  constructor() {
4    this.listeners = new Map();
5  }
6
7  on(event, callback) {
8    if (!this.listeners.has(event)) {
9      this.listeners.set(event, new Set());
10    }
11    this.listeners.get(event).add(callback);
12
13    // Return cleanup function
14    return () => {
15      this.listeners.get(event)?.delete(callback);
16    };
17  }
18
19  emit(event, payload) {
20    if (this.listeners.has(event)) {
21      this.listeners.get(event).forEach(cb => cb(payload));
22    }
23  }
24
25  off(event, callback) {
26    this.listeners.get(event)?.delete(callback);
27  }
28}
29
30// Singleton -- one bus for the entire application
31const eventBus = new EventBus();

Custom hooks for event handling

1// Hook for listening to events
2function useEventListener(event, handler) {
3  const savedHandler = useRef(handler);
4
5  useEffect(() => {
6    savedHandler.current = handler;
7  }, [handler]);
8
9  useEffect(() => {
10    const listener = (payload) => savedHandler.current(payload);
11    const unsubscribe = eventBus.on(event, listener);
12    return unsubscribe; // Cleanup on unmount
13  }, [event]);
14}
15
16// Hook for emitting events
17function useEventEmitter() {
18  return useCallback((event, payload) => {
19    eventBus.emit(event, payload);
20  }, []);
21}

Practical example: Ship alert system

1// Engine component -- emits events
2function EnginePanel() {
3  const emit = useEventEmitter();
4
5  const setSpeed = (speed) => {
6    emit('engine:speedChanged', { speed, timestamp: Date.now() });
7    emit('ship:log', { source: 'Engine', message: `Speed: ${speed}` });
8  };
9
10  return (
11    <div className="panel">
12      <h3>Engine</h3>
13      <button onClick={() => setSpeed('MAX')}>MAX</button>
14      <button onClick={() => setSpeed('CRUISE')}>CRUISE</button>
15      <button onClick={() => setSpeed('STOP')}>STOP</button>
16    </div>
17  );
18}
19
20// Display component -- listens for events
21function SpeedDisplay() {
22  const [speed, setSpeed] = useState('IDLE');
23
24  useEventListener('engine:speedChanged', (data) => {
25    setSpeed(data.speed);
26  });
27
28  return <div className="display">Speed: {speed}</div>;
29}
30
31// Log component -- listens to all logs
32function ShipLog() {
33  const [entries, setEntries] = useState([]);
34
35  useEventListener('ship:log', (data) => {
36    setEntries(prev => [
37      { ...data, time: new Date().toLocaleTimeString() },
38      ...prev,
39    ].slice(0, 10));
40  });
41
42  return (
43    <ul className="log">
44      {entries.map((entry, i) => (
45        <li key={i}>[{entry.time}] {entry.source}: {entry.message}</li>
46      ))}
47    </ul>
48  );
49}

Notice that

EnginePanel
does not import
SpeedDisplay
or
ShipLog
. The components are completely independent. You can remove
ShipLog
from the component tree and
EnginePanel
still works without any changes.

Native EventTarget API

Instead of a custom EventBus, you can use the browser's native

EventTarget
API:

1// EventTarget -- built into the browser
2const shipEvents = new EventTarget();
3
4function useNativeEventListener(eventName, handler) {
5  useEffect(() => {
6    const listener = (e) => handler(e.detail);
7    shipEvents.addEventListener(eventName, listener);
8    return () => shipEvents.removeEventListener(eventName, listener);
9  }, [eventName, handler]);
10}
11
12function emitNativeEvent(eventName, detail) {
13  shipEvents.dispatchEvent(new CustomEvent(eventName, { detail }));
14}

Advanced EventBus with middleware

You can extend the EventBus with middleware -- functions that intercept events before delivery:

1class AdvancedEventBus extends EventBus {
2  constructor() {
3    super();
4    this.middleware = [];
5  }
6
7  use(middlewareFn) {
8    this.middleware.push(middlewareFn);
9  }
10
11  emit(event, payload) {
12    // Pass through middleware
13    let finalPayload = payload;
14    for (const mw of this.middleware) {
15      finalPayload = mw(event, finalPayload);
16      if (finalPayload === null) return; // Middleware blocked the event
17    }
18    super.emit(event, finalPayload);
19  }
20}
21
22// Middleware: logging
23advancedBus.use((event, payload) => {
24  console.log(`[Event] ${event}`, payload);
25  return payload;
26});
27
28// Middleware: validation
29advancedBus.use((event, payload) => {
30  if (event === 'engine:speedChanged' && payload.speed === 'MAX') {
31    if (!window.confirm('Full power for sure?')) return null; // Block!
32  }
33  return payload;
34});

EDA vs Context API vs Props

| Feature | Props | Context API | Event-Driven | |---------|-------|-------------|-------------| | Direction | Parent -> Child | Ancestor -> Descendants | Any | | Coupling | Tight | Medium | Loose | | Re-renders | Direct | All consumers | Only subscribers | | Cross-branch communication | Impossible | Requires common ancestor | Direct | | Debuggability | Easy | Medium | Requires logging |

When to use Event-Driven Architecture?

  • Communication between distant components -- you don't want to pass props through 5 levels
  • Notification/alert system -- many sources generate events, one component displays them
  • Logging and analytics -- user events emitted once, listened to by many systems
  • Real-time updates -- data updated in real time (WebSocket -> EventBus -> components)

EDA is like a central communication system on a spaceship. Every module can send and receive messages without a direct connection to other modules. The key to success is remembering about cleanup (unsubscribing) when components unmount -- otherwise you'll create memory leaks, like a broken relay that still consumes energy!

Go to CodeWorlds