DEV Community

Cover image for Simple Event BUS in Javascript
artydev
artydev

Posted on

Simple Event BUS in Javascript

It's sometimes surprising how just few lines of codes can be very usefull

    /* ------------------------------
      EventBus
    ------------------------------ */
    class EventBus {
      constructor() {
        this.listeners = new Map()
      }
      on(event, handler) {
        let arr = this.listeners.get(event)
        if (arr) arr.push(handler)
        else this.listeners.set(event, [handler])
        return this
      }
      emit(event, data) {
        const arr = this.listeners.get(event)
        if (!arr) return
        const copy = arr.slice()
        for (let fn of copy) fn(data)
      }
    }

    const bus = new EventBus()


    /* ------------------------------
      UI Elements
    ------------------------------ */
    const avatar = document.getElementById("avatar")
    const nameEl = document.getElementById("name")
    const emailEl = document.getElementById("email")
    const locEl = document.getElementById("location")
    const statusEl = document.getElementById("status")
    const btn = document.getElementById("fetchBtn")


    /* ------------------------------
      Fetch Random User
    ------------------------------ */
    async function fetchRandomUser() {
      statusEl.innerHTML = "<div class='loading'>Loading user...</div>"

      try {
        const res = await fetch("https://randomuser.me/api/")
        if (!res.ok) throw new Error("Network error")

        const data = await res.json()
        const user = data.results[0]

        bus.emit("user.loaded", user)
      } catch (err) {
        bus.emit("user.error", err)
      }
    }


    /* ------------------------------
      Event Handlers
    ------------------------------ */

    // When user is loaded
    bus.on("user.loaded", (user) => {
      statusEl.innerHTML = ""

      avatar.src = user.picture.large
      avatar.hidden = false

      nameEl.textContent = `${user.name.first} ${user.name.last}`
      emailEl.textContent = user.email
      locEl.textContent = `${user.location.city}, ${user.location.country}`
    })

    // On error
    bus.on("user.error", (err) => {
      statusEl.innerHTML = `<div class='error'>Error: ${err.message}</div>`
    })


    /* ------------------------------
      Button action
    ------------------------------ */
    btn.addEventListener("click", fetchRandomUser)

    // Auto-load on startup
    fetchRandomUser()
Enter fullscreen mode Exit fullscreen mode

You can test the demo here : EventBus

Here is another version with State Managment :

/* ------------------------------
      EventBus (For Action/Flow Events)
    ------------------------------ */
    class EventBus {
      constructor() {
        this.listeners = new Map()
      }
      on(event, handler) {
        let arr = this.listeners.get(event)
        if (arr) arr.push(handler)
        else this.listeners.set(event, [handler])
        return this
      }
      emit(event, data) {
        const arr = this.listeners.get(event)
        if (!arr) return
        const copy = arr.slice()
        for (let fn of copy) fn(data)
      }
    }

    const bus = new EventBus()


    /* ------------------------------
      StateManager (For Data State)
    ------------------------------ */
    class StateManager {
      constructor(initialState) {
        this.state = initialState;
        this.listeners = []; // Functions to call when state changes
      }

      getState() {
        return this.state;
      }

      // Updates the state and notifies all subscribers
      setState(newState) {
        this.state = { ...this.state,
          ...newState
        }; // Merge new state with existing
        this.notifyListeners();
      }

      // Adds a function to be called on state changes
      subscribe(listener) {
        this.listeners.push(listener);
        // Immediately call the listener with the current state for initial render
        listener(this.state);
      }

      // Notifies all subscribed listeners with the current state
      notifyListeners() {
        for (const listener of this.listeners) {
          listener(this.state);
        }
      }
    }

    // Initialize the state manager with an initial state
    const stateManager = new StateManager({
      user: null,
      loading: false,
      error: null,
    });


    /* ------------------------------
      UI Elements
    ------------------------------ */
    const avatar = document.getElementById("avatar")
    const nameEl = document.getElementById("name")
    const emailEl = document.getElementById("email")
    const locEl = document.getElementById("location")
    const statusEl = document.getElementById("status")
    const btn = document.getElementById("fetchBtn")


    /* ------------------------------
      Fetch Random User
    ------------------------------ */
    async function fetchRandomUser() {
      // 1. **ACTION:** Inform the world that the loading process is starting
      bus.emit("fetch.start");

      try {
        const res = await fetch("https://randomuser.me/api/")
        if (!res.ok) throw new Error("Network error")

        const data = await res.json()
        const user = data.results[0]

        // 2. **ACTION:** Inform the world that the user loaded successfully
        bus.emit("user.loaded", user)
      } catch (err) {
        // 2. **ACTION:** Inform the world that an error occurred
        bus.emit("user.error", err)
      }
    }


    /* ------------------------------
      Event Handlers (Update State)
    ------------------------------ */

    // When fetch starts
    bus.on("fetch.start", () => {
      // Set the central state for loading
      stateManager.setState({
        loading: true,
        error: null,
        user: null
      });
    });

    // When user is loaded
    bus.on("user.loaded", (user) => {
      // Set the central state with the new user data
      stateManager.setState({
        user: user,
        loading: false,
        error: null
      });
    });

    // On error
    bus.on("user.error", (err) => {
      // Set the central state with the error
      stateManager.setState({
        error: err,
        loading: false,
        user: null
      });
    });


    /* ------------------------------
      UI Update Function (Reacts to state changes)
    ------------------------------ */
    // This function acts as the single listener to the StateManager
    function updateUI(state) {
      // Handle loading status
      if (state.loading) {
        statusEl.innerHTML = "<div class='loading'>Loading user...</div>";
        avatar.hidden = true;
        nameEl.textContent = "";
        emailEl.textContent = "";
        locEl.textContent = "";
        return;
      }

      // Handle errors
      if (state.error) {
        statusEl.innerHTML = `<div class='error'>Error: ${state.error.message}</div>`;
        avatar.hidden = true;
        nameEl.textContent = "";
        emailEl.textContent = "";
        locEl.textContent = "";
        return;
      }

      // Handle user data display
      if (state.user) {
        statusEl.innerHTML = "";
        avatar.src = state.user.picture.large;
        avatar.hidden = false;
        nameEl.textContent = `${state.user.name.first} ${state.user.name.last}`;
        emailEl.textContent = state.user.email;
        locEl.textContent = `${state.user.location.city}, ${state.user.location.country}`;
      } else {
        // If no user and not loading/error, clear everything (e.g., initial state)
        statusEl.innerHTML = "";
        avatar.hidden = true;
        nameEl.textContent = "";
        emailEl.textContent = "";
        locEl.textContent = "";
      }
    }


    /* ------------------------------
      Button action & State Subscription
    ------------------------------ */
    btn.addEventListener("click", fetchRandomUser)

    // The UI is the ONLY subscriber to the state manager
    stateManager.subscribe(updateUI);

    // Auto-load on startup
    fetchRandomUser()
Enter fullscreen mode Exit fullscreen mode

Demo

Top comments (0)