Event-Driven Communication in frontend/react: Beginner's Guide

Article Banner
Author(s): Ajay Kumar
Last updated: 19 Jul 2025

🎭 The Tale of Two Components: A Story of Decoupled Communication


Imagine you're at a bustling coffee shop. People are chatting, baristas are calling out orders, and the atmosphere is alive with activity. But here's the fascinating part: the barista doesn't need to know every customer personally, and customers don't need to know each other. They all communicate through a simple, elegant system—the order board and the coffee shop's rhythm.

This is exactly how event-driven communication works in micro-frontends. It's like having a magical post office where components can send messages to each other without ever meeting face-to-face. Today, we'll explore this fascinating world where components become pen pals, communicating through a shared event system.

🔮 The Magic of Event-Driven Architecture


Picture this: You have a React application with multiple components that need to talk to each other. Traditionally, you'd connect them like a family tree—parent to child, child to grandchild, creating a web of dependencies that's as tangled as Christmas lights.

But what if there was a better way? What if components could communicate like people at a party—sending messages into the air and letting anyone who's interested pick them up?

The Event Bus: Our Digital Post Office


Think of an event bus as a magical post office that never closes. When one component wants to share something, it simply drops a message into the post office. Any other component that's interested can pick up that message and react accordingly.

The beauty of this system is that the sender doesn't need to know who's listening, and the listeners don't need to know who's sending. It's like having a town square where anyone can make an announcement, and anyone can choose to listen.

The Three Musketeers of Event Communication


Every event-driven system has three key players:

  • The Publisher (Sender): Like a town crier, this component announces events to the world
  • The Subscriber (Listener): Like an attentive audience, this component listens for specific events
  • The Event Bus: Like the town square, this is where all the magic happens

🏗️ Building Our Digital Town Square


Let's create our own event bus—a simple but powerful system that can handle all our component communication needs. Think of it as building a digital town square where messages can flow freely.


// Simple Event Bus for decoupled communication
class EventBus {
  constructor() {
    this.events = {};
  }

  // Subscribe to an event
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // Return unsubscribe function
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }

  // Emit an event with data
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => {
        callback(data);
      });
    }
  }

  // Remove all listeners for an event
  off(event) {
    if (this.events[event]) {
      delete this.events[event];
    }
  }
}

// Create a singleton instance
const eventBus = new EventBus();

export default eventBus;
        
Code Sample #1 : Our Digital Town Square - The Event Bus

This simple event bus is like having a magical notice board where anyone can post messages and anyone can read them. The beauty is in its simplicity—it's just a collection of functions that know how to deliver messages to the right people.

🎭 Meet Our Characters: The Sender and Receiver


Now let's meet our two main characters in this story: the Sender and the Receiver. They're like two people at opposite ends of our digital town square, communicating through our event bus.

The Sender: Our Town Crier


The Sender is like a friendly town crier who loves to share news with everyone. It doesn't care who's listening—it just wants to broadcast its messages to the world.


import React, { useState } from 'react';
import eventBus from '../utils/eventBus';
import './Sender.css';

const Sender = () => {
  const [message, setMessage] = useState('');
  const [eventType, setEventType] = useState('message');

  const handleSendMessage = () => {
    if (message.trim()) {
      // Emit the event with message data
      eventBus.emit(eventType, {
        message: message,
        timestamp: new Date().toISOString(),
        sender: 'Sender Component'
      });
      
      // Clear the input after sending
      setMessage('');
    }
  };

  const handleSendAlert = () => {
    eventBus.emit('alert', {
      type: 'info',
      message: 'This is an alert from Sender!',
      timestamp: new Date().toISOString()
    });
  };

  const handleSendError = () => {
    eventBus.emit('error', {
      type: 'error',
      message: 'This is an error from Sender!',
      timestamp: new Date().toISOString()
    });
  };

  return (
    <div className="sender">
      <h2>Sender Component</h2>
      <p>This component emits events that the Receiver listens to.</p>
      
      <div className="sender-controls">
        <div className="input-group">
          <label htmlFor="eventType">Event Type:</label>
          <select 
            id="eventType"
            value={eventType} 
            onChange={(e) => setEventType(e.target.value)}
          >
            <option value="message">message</option>
            <option value="alert">alert</option>
            <option value="error">error</option>
            <option value="custom">custom</option>
          </select>
        </div>

        <div className="input-group">
          <label htmlFor="message">Message:</label>
          <input
            id="message"
            type="text"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            placeholder="Enter your message..."
            onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
          />
        </div>

        <button 
          onClick={handleSendMessage}
          disabled={!message.trim()}
          className="send-btn"
        >
          Send Message
        </button>
      </div>

      <div className="quick-actions">
        <h3>Quick Actions:</h3>
        <button onClick={handleSendAlert} className="alert-btn">
          Send Alert
        </button>
        <button onClick={handleSendError} className="error-btn">
          Send Error
        </button>
      </div>
    </div>
  );
};

export default Sender;
            
Code Sample #2 : The Sender Component - Our Town Crier

The Receiver: Our Attentive Audience


The Receiver is like an attentive audience member who's always listening for interesting announcements. It doesn't care who's speaking—it just wants to hear what's being said.


import React, { useState, useEffect, useCallback } from 'react';
import eventBus from '../utils/eventBus';
import './Receiver.css';

const Receiver = () => {
  const [receivedEvents, setReceivedEvents] = useState([]);
  const [isListening, setIsListening] = useState(true);

  const handleEvent = useCallback((data) => {
    if (isListening) {
      setReceivedEvents(prev => [{
        ...data,
        id: Date.now(),
        receivedAt: new Date().toISOString()
      }, ...prev]);
    }
  }, [isListening]);

  useEffect(() => {
    // Subscribe to different event types
    const unsubscribeMessage = eventBus.on('message', handleEvent);
    const unsubscribeAlert = eventBus.on('alert', handleEvent);
    const unsubscribeError = eventBus.on('error', handleEvent);
    const unsubscribeCustom = eventBus.on('custom', handleEvent);

    // Cleanup function to unsubscribe when component unmounts
    return () => {
      unsubscribeMessage();
      unsubscribeAlert();
      unsubscribeError();
      unsubscribeCustom();
    };
  }, [handleEvent]);

  const clearEvents = () => {
    setReceivedEvents([]);
  };

  const toggleListening = () => {
    setIsListening(!isListening);
  };

  const getEventTypeClass = (eventType) => {
    switch (eventType) {
      case 'alert':
        return 'event-alert';
      case 'error':
        return 'event-error';
      case 'custom':
        return 'event-custom';
      default:
        return 'event-message';
    }
  };

  return (
    <div className="receiver">
      <h2>Receiver Component</h2>
      <p>This component listens to events from the Sender component.</p>
      
      <div className="receiver-controls">
        <button 
          onClick={toggleListening}
          className={isListening ? 'listening-btn active' : 'listening-btn inactive'}
        >
          {isListening ? '🔊 Listening' : '🔇 Not Listening'}
        </button>
        
        <button onClick={clearEvents} className="clear-btn">
          Clear Events
        </button>
      </div>

      <div className="events-container">
        <h3>Received Events ({receivedEvents.length})</h3>
        
        {receivedEvents.length === 0 ? (
          <div className="no-events">
            <p>No events received yet. Try sending a message from the Sender!</p>
          </div>
        ) : (
          <div className="events-list">
            {receivedEvents.map((event) => (
              <div 
                key={event.id} 
                className={`event-item ${getEventTypeClass(event.eventType || 'message')}`}
              >
                <div className="event-header">
                  <span className="event-type">
                    {event.eventType || 'message'}
                  </span>
                  <span className="event-time">
                    {new Date(event.receivedAt).toLocaleTimeString()}
                  </span>
                </div>
                
                <div className="event-content">
                  <p className="event-message">
                    {event.message || 'No message content'}
                  </p>
                  
                  {event.sender && (
                    <p className="event-sender">
                      From: {event.sender}
                    </p>
                  )}
                  
                  {event.type && (
                    <p className="event-type-info">
                      Type: {event.type}
                    </p>
                  )}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default Receiver;
            
Code Sample #3 : The Receiver Component - Our Attentive Audience

🌟 The Magic Happens: Multiple Listeners


Here's where the real magic begins. Just like in a real town square, one announcement can be heard by many people, each reacting in their own way. Let's add a third character to our story—the Logger.

The Logger: Our Town Historian


The Logger is like a town historian who records everything that happens in the square. While the Receiver displays messages, the Logger keeps a permanent record of all events.


import React, { useState, useEffect, useCallback } from 'react';
import eventBus from '../utils/eventBus';
import './Logger.css';

const Logger = () => {
  const [logs, setLogs] = useState([]);
  const [isLogging, setIsLogging] = useState(true);
  const [logLevel, setLogLevel] = useState('all'); // all, message, alert, error, custom

  const getLogLevel = (data) => {
    if (data.type === 'error') return 'error';
    if (data.type === 'info') return 'info';
    if (data.eventType === 'alert') return 'warning';
    return 'info';
  };

  const handleLog = useCallback((data) => {
    if (isLogging) {
      const logEntry = {
        id: Date.now(),
        timestamp: new Date().toISOString(),
        eventType: data.eventType || 'unknown',
        message: data.message || 'No message',
        sender: data.sender || 'Unknown',
        type: data.type || 'info',
        level: getLogLevel(data)
      };

      setLogs(prev => [logEntry, ...prev]);
    }
  }, [isLogging]);

  useEffect(() => {
    // Subscribe to all event types for logging
    const unsubscribeMessage = eventBus.on('message', handleLog);
    const unsubscribeAlert = eventBus.on('alert', handleLog);
    const unsubscribeError = eventBus.on('error', handleLog);
    const unsubscribeCustom = eventBus.on('custom', handleLog);

    return () => {
      unsubscribeMessage();
      unsubscribeAlert();
      unsubscribeError();
      unsubscribeCustom();
    };
  }, [handleLog]);

  const clearLogs = () => {
    setLogs([]);
  };

  const toggleLogging = () => {
    setIsLogging(!isLogging);
  };

  const getLogLevelClass = (level) => {
    switch (level) {
      case 'error':
        return 'log-error';
      case 'warning':
        return 'log-warning';
      case 'info':
        return 'log-info';
      default:
        return 'log-info';
    }
  };

  const filteredLogs = logs.filter(log => {
    if (logLevel === 'all') return true;
    return log.level === logLevel;
  });

  return (
    <div className="logger">
      <h2>Event Logger</h2>
      <p>This component logs all events for debugging and monitoring purposes.</p>
      
      <div className="logger-controls">
        <div className="control-group">
          <button 
            onClick={toggleLogging}
            className={isLogging ? 'logging-btn active' : 'logging-btn inactive'}
          >
            {isLogging ? '📝 Logging' : '⏸️ Paused'}
          </button>
          
          <button onClick={clearLogs} className="clear-btn">
            Clear Logs
          </button>
        </div>

        <div className="filter-group">
          <label htmlFor="logLevel">Filter Level:</label>
          <select 
            id="logLevel"
            value={logLevel} 
            onChange={(e) => setLogLevel(e.target.value)}
          >
            <option value="all">All Levels</option>
            <option value="error">Error</option>
            <option value="warning">Warning</option>
            <option value="info">Info</option>
          </select>
        </div>
      </div>

      <div className="logs-container">
        <h3>Event Logs ({filteredLogs.length})</h3>
        
        {filteredLogs.length === 0 ? (
          <div className="no-logs">
            <p>No logs yet. Events will appear here when sent from the Sender!</p>
          </div>
        ) : (
          <div className="logs-list">
            {filteredLogs.map((log) => (
              <div 
                key={log.id} 
                className={`log-item ${getLogLevelClass(log.level)}`}
              >
                <div className="log-header">
                  <span className="log-level">
                    {log.level.toUpperCase()}
                  </span>
                  <span className="log-time">
                    {new Date(log.timestamp).toLocaleTimeString()}
                  </span>
                  <span className="log-type">
                    {log.eventType}
                  </span>
                </div>
                
                <div className="log-content">
                  <p className="log-message">
                    {log.message}
                  </p>
                  
                  {log.sender && (
                    <p className="log-sender">
                      From: {log.sender}
                    </p>
                  )}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default Logger;
            
Code Sample #4 : The Logger Component - Our Town Historian

💡 The Power of Multiple Listeners

This is the true beauty of event-driven communication: one sender can notify multiple receivers independently. The Sender doesn't need to know about the Receiver or Logger—it simply announces its message, and anyone who's interested can listen and react in their own way.

🎨 The Art of Event Design


Just like a well-designed town square has clear signage and organized notice boards, our event system needs thoughtful design to be truly effective.

Naming Your Events: The Art of Clarity


Event names are like street signs—they need to be clear, consistent, and meaningful. A good event name tells you exactly what happened, like a well-written headline.


// Good event names (like clear street signs)
const GOOD_EVENTS = {
  'user:login': 'user:login',           // Clear and specific
  'cart:item:added': 'cart:item:added', // Hierarchical and descriptive
  'product:viewed': 'product:viewed',   // Action-oriented
  'theme:changed': 'theme:changed'      // Simple and direct
};

// Bad event names (like confusing street signs)
const BAD_EVENTS = {
  'update': 'update',                   // Too vague
  'data': 'data',                       // Not descriptive
  'event1': 'event1',                   // Meaningless
  'doSomething': 'doSomething'          // Not clear what it does
};
            
Code Sample #5 : Event Naming Patterns

Event Data: The Message in the Bottle


Event data is like the message inside a bottle—it needs to contain everything the receiver needs to understand what happened, but not so much that it becomes unwieldy.


// Good event data (like a well-written letter)
eventBus.emit('user:login', {
  userId: '12345',
  email: 'user@example.com',
  timestamp: new Date().toISOString(),
  source: 'login-form',
  sessionId: 'abc-123-def'
});

// Bad event data (like a confusing note)
eventBus.emit('login', {
  data: user,           // Too generic
  stuff: 'whatever',    // Meaningless
  // Missing important context
});
            
Code Sample #6 : Well-Structured Event Data

🔄 The Dance of Decoupling


Now let's explore why this dance of decoupled communication is so powerful. It's like having a party where everyone can mingle freely, rather than being stuck in assigned seats.

The Problem with Traditional Coupling


Traditional component communication is like a family dinner where everyone has to pass messages through the person sitting next to them. If you want to tell your cousin something, you have to ask your sibling to tell their parent to tell your cousin. It's inefficient and creates a web of dependencies.


// Like a family dinner with assigned seating
class UserService {
  constructor(cartService, preferencesService, notificationsService) {
    this.cartService = cartService;
    this.preferencesService = preferencesService;
    this.notificationsService = notificationsService;
  }

  updateUser(user) {
    // Have to tell everyone individually
    this.cartService.updateUser(user);
    this.preferencesService.updateUser(user);
    this.notificationsService.updateUser(user);
  }
}
            
Code Sample #7 : Traditional Coupled Communication

The Freedom of Event-Driven Communication


Event-driven communication is like a party where everyone can mingle freely. You can announce something to the room, and anyone who's interested can listen and react in their own way.


// Like a party where everyone can mingle freely
class UserService {
  updateUser(user) {
    // Simply announce to the room
    eventBus.emit('user:updated', { user });
  }
}

// Anyone interested can listen and react
eventBus.on('user:updated', (data) => {
  // Cart service reacts
  updateCartForUser(data.user);
});

eventBus.on('user:updated', (data) => {
  // Preferences service reacts
  updatePreferencesForUser(data.user);
});

eventBus.on('user:updated', (data) => {
  // Notifications service reacts
  sendWelcomeNotification(data.user);
});
            
Code Sample #8 : Decoupled Event-Driven Communication

🌟 Real-World Applications: When Events Shine


Event-driven communication isn't just a theoretical concept—it's a practical solution for many real-world scenarios. Let's explore some situations where this pattern truly shines.

The Shopping Experience


Imagine an e-commerce application where multiple components need to react to user actions. When a user adds an item to their cart, several things need to happen:

  • The cart needs to update
  • The inventory needs to be checked
  • Recommendations need to be updated
  • Analytics need to be tracked
  • A notification might need to be shown

With event-driven communication, the "add to cart" component simply announces what happened, and all interested components can react independently.

The User Authentication Flow


When a user logs in, multiple parts of the application need to know about it:

  • The navigation bar needs to show the user's name
  • The cart needs to load the user's saved items
  • Preferences need to be loaded
  • Permissions need to be updated
  • Analytics need to track the login

Instead of the login component having to know about all these other components, it simply announces "user logged in" and lets everyone else figure out what they need to do.

🎭 The Dark Side: When Events Can Be Tricky


Like any powerful tool, event-driven communication has its challenges. It's important to understand when to use it and when to avoid it.

When Events Are Perfect


  • Multiple components need to react to the same event - Like when a user logs in and many parts of the app need to update
  • You want to avoid tight coupling - When components shouldn't need to know about each other
  • Real-time updates are needed - When changes need to propagate immediately across the application
  • You're building a micro-frontend architecture - Where different teams work on different parts

When Events Might Not Be the Best Choice


  • Simple parent-child communication - When you just need to pass data down one level
  • Guaranteed delivery is critical - When you need to ensure a message is received
  • Performance is extremely critical - When the overhead of events might be too much
  • Synchronous operations - When you need immediate, blocking responses

🚀 Bringing It All Together: Our Demo in Action


Our demo from the [frontend-events-demo repository](https://github.com/ajaysskumar/frontend-events-demo) brings all these concepts to life. It's like a miniature town square where you can see the magic of event-driven communication happening in real-time.

The demo features three main characters:

  • The Sender - Our town crier who announces messages to the world
  • The Receiver - Our attentive audience who displays messages in real-time
  • The Logger - Our town historian who keeps a permanent record
Event-driven communication flow in the demo repository - showing how messages flow from Sender through Event Bus to multiple listeners (Receiver and Logger)
Figure 1 : Event-driven communication flow in the demo repository - showing how messages flow from Sender through Event Bus to multiple listeners (Receiver and Logger)
Live demo showing the event-driven communication in action - Sender component broadcasting messages that are received by both Receiver and Logger components simultaneously
Figure 2 : Live demo showing the event-driven communication in action - Sender component broadcasting messages that are received by both Receiver and Logger components simultaneously

You can interact with the demo to see how one announcement can be heard by multiple listeners, each reacting in their own unique way. It's a perfect example of how components can communicate without ever knowing about each other directly.

🎯 The Moral of Our Story


Event-driven communication is like having a magical town square where everyone can communicate freely. It's not about forcing components to talk to each other—it's about creating a space where they can choose to listen and react in their own way.

The beauty of this approach is its simplicity and flexibility. Components become like people at a party—they can mingle, share information, and react to what's happening around them without being tied to specific relationships.

Whether you're building a simple React app or a complex micro-frontend architecture, event-driven communication provides a clean, scalable way for components to work together while maintaining their independence. It's like having the best of both worlds—the power of collaboration with the freedom of independence.

🔗 Reference Implementation


All code samples above are from the open-source frontend-events-demo repository. You can clone, run, and extend the demo to fit your own business needs.

For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/frontend-events-demo

Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com

Copyright © 2025 Dev Codex

An unhandled error has occurred. Reload 🗙