DEV Community

Cover image for Simplify External Service Communication in React using Event Driven Architecture
Shweta Kale
Shweta Kale

Posted on

Simplify External Service Communication in React using Event Driven Architecture

Have you ever found yourself working on a project, trying to integrate Amplitude or Mixpanel to track user events in your application? Well I have. Initially, it seems straightforward - you call the API on a button click or text blur event, and voila you are tracking user interactions like a pro.

But here's the kicker: what happens when your project starts to grow, and you realize you need to integrate more service APIs using the same events? Suddenly, updating the onclick method feels like untangling spaghetti code in the dark. You keep on adding more and more APIs directly in your onclick method and this makes your code tightly coupled. Just to add one more service you have to edit all the files which could have been avoided if we used a different approach.

Feel like you have been in this situation? In this post I will discuss how we can make use of Event Driven architecture to make the code loosely coupled so you don't have to edit the onClick method to add a new integration.

What is Event Driven Architecture in React?

Instead of components being tightly coupled and constantly communicating with each other, we invoke event handlers for specific event.

They will only react when something happens – like a user clicking a button or data being updated. This way, components can work independently without knowing too much about each other, making the system more flexible and easier to change.

Why should we use an event listener to communicate with Amplitude when we also need to call the listener onClick? Does it make any difference?

Using an event listener to register event and then communicate with amplitude even if we also need to add the listener in the onClick, can make a significant difference in how our application is structured and how it scales. While it may seem like adding an extra layer of complexity, especially if we are calling event listeners onClick, they may provide several advantage like:

  1. Decoupling logic: We decouple the logic of handling Amplitude events from specific UI interactions. This means that changes to UI elements or the way events are triggered won't directly impact the integration with amplitude.

  2. Scalability: We can add or remove multiple third party integrations without having to modify individual UI components. It promotes cleaner, more modular code that's easier to maintain and extend.

  3. Consistency: Using event listeners ensure consistency in how we handle Amplitude events throughout our application. By defining a standard interface for interacting with amplitude, we reduce the risk of inconsistency or errors that can arise from manually calling listeners onClick in different part of codebase.

In summary, while it may seem redundant to use an event listener alongside onClick handlers, adopting this approach can lead to a more robust, scalable, and maintainable architecture when integrating with third-party services. It promotes loose coupling, scalability, reusability, and consistency, making our application more resilient to changes and easier to manage in the long run.

Show me the code

You can create custom events and inject listeners that respond to those events. Each listener can be programmed to execute specific actions, here calling external APIs

// event.js

export function emitEvent(ev, payload) {
  const event = new CustomEvent(ev, {
    detail: payload,
  });
  document.dispatchEvent(event);
}

export function initInternalEvents() {
   document.addEventListeners('signup_click', (payload) => 
   {
     amplitude.track('Signup Button Clicked', payload);
   })

   document.addEventListeners('video_played', (payload) => 
   {
     amplitude.track('Video Played', payload);
   })
}
Enter fullscreen mode Exit fullscreen mode

In the above code we have two methods - emitEvent and initInternalEvents.

  • The emit event method is called to trigger the call to external APIs when user execute the desired action.
  • initInternalEvents is used to register an event listener on the target object
  // signup Page 

import React from 'react';
import { initInternalEvents } from './event'; // Import the initInternalEvents function

function SignupPage() {
  // Function to handle signup button click
  const handleSignupClick = () => {
    // Simulate payload data for signup event
    const signupPayload = {
      user_id: '123456',
      source: 'website',
    };

    // Call the emit method to track the signup button click event
    emitEvent('signup_click', signupPayload);
  };

  return (
    <div>
      <h1>Sign Up Page</h1>
      <button onClick={handleSignupClick}>Sign Up</button>
    </div>
  );
}

export default SignupPage;

Enter fullscreen mode Exit fullscreen mode

In above code we have used emitEvent method to send the signup data to the Amplitue. You can similarly add other methods that needs to be called when userSignup and pass the paylod.

So we have called the emitEvent method, but dispatch won't work without we register the event. You need to call register Event listener in the root or app.jsx file such that it will be called only once.

import React from 'react';
import SignupPage from './SignupPage';
import { initInternalEvents } from './event'; // Import the initInternalEvents function

function App() {
  useEffect(()=>{
   // Initialize internal events
   initInternalEvents();
  }, [])

  return (
    <div className="App">
      <h1>Welcome to My App</h1>
      <SignupPage />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

This is all you need. Let's summarize how this works - you have various events like "UserLoggedIn", "ItemAddedToCart", or "OrderPlaced". When these events occur within your application, you can have listeners attached to them. Each listener can handle the event by executing tasks like logging the user's activity, sending data to analytics, or tracking the data.

The beauty of this approach lies in its flexibility and modularity. By decoupling the event triggers from the actions they initiate, you can easily extend or modify your application's behavior without directly modifying the event triggers themselves. This promotes a more scalable and maintainable architecture, as you can add new functionalities or integrate with different services by simply attaching new listeners to existing events.

What do you think about this approach and how do you handle calling external APIs to log user events? Let me know in the comments below.

Top comments (0)