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.
Event-Driven Architecture (EDA) is based on:
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();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}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.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}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});| 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 |
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!