• Where Event-Driven UI Hides Its Dependencies

    A man pulls a cord beside an exposed wall frame. Ropes run over pulleys to three brass bells above the adjoining rooms.

    Adding an item to a cart should not require the cart API to know where the badge lives. Publish an event, let the badge listen, and adding analytics no longer means opening the cart module again. That is a useful amount of independence.

    Then the badge shows the wrong number. The request succeeded. The event fired. Somewhere between those two facts and the number on screen, something went wrong. The cart module, quite correctly, knows nothing about it.

    In this example, I want to follow a change from the request to the screen, including the bits an event name lets us skip over.

    A Name Instead of an Import

    The original version of this post used jQuery events on body. A standalone EventTarget, created with new EventTarget(), gives us the same basic arrangement without borrowing a DOM node. Keep one for the cart, and the cart module can announce a change without importing the header, the item rows, or the mini-cart.

    Those components still need to agree on something. They share an event name, a payload shape, and an understanding of when the event happens. Does cart:changed mean the user clicked a button, the request was sent, or the server accepted the change? All three are plausible moments to update a number. A badge showing confirmed state needs the last one.

    For this cart, publish after a successful response. The quantity and totals should describe the same accepted state. A listener should not have to guess whether the line total belongs to the quantity it just received.

    The cart module no longer names its consumers. Whoever debugs the badge still needs to find them. Following the change now means searching for subscriptions to cart:changed and checking what each listener assumes.

    The Badge Arrives Late

    Suppose the badge mounts after an item has been added. It subscribes correctly, on the right target, with the right event name. It stays at its initial value until the next change.

    Nothing failed inside the event system. An EventTarget notifies its current listeners. It keeps no history for a component that turns up afterwards. Making the payload more detailed will not help a listener that never received it.

    The badge needs somewhere to read the current cart. That state has to outlive the badge, and it has to be updated before listeners are notified. In a framework application, I would use the framework’s state model or an existing store. For a small vanilla UI, the connection can look like this:

    javascript
    export function mountCartBadge(element, cart) {
      const controller = new AbortController();
    
      function render() {
        element.textContent = String(cart.getSnapshot().itemCount);
      }
    
      cart.events.addEventListener('cart:changed', render, {
        signal: controller.signal,
      });
      render();
    
      return () => controller.abort();
    }

    Here, cart is an application-owned store, not a browser API. Its initial data has already loaded; getSnapshot() synchronously returns the current state, including the total number of items. Its events target announces a change after that state has been replaced. Fetching the initial cart and showing a loading or error state belong to the code that prepares it.

    The badge reads the snapshot when it mounts, then reads it again on each notification. It no longer needs to have attended every previous cart update. The component owner calls the returned cleanup function when removing the badge; the signal option removes the listener when the controller is aborted. Without that cleanup, a long-lived cart can keep updating a DOM node that has already left the page.

    This fixes the late subscription. If several cart requests can overlap, the store still needs a policy for accepting their responses. Saving the last response to arrive is only correct if it is also the newest state. An event cannot make that decision for us.

    When a Listener Becomes a Step

    There is a more awkward dependency when one listener needs another listener to finish first.

    Suppose a quantity-change listener recalculates the total, while a second listener displays it. Now the display depends on the calculation running first. Move the subscriptions around and the same event can leave a different number on screen. The dependency is easy to miss because neither listener calls the other.

    For the cart in this example, the server already returns the quantity and totals together. Apply that response as one state update and notify listeners afterwards. Splitting it into separate quantity, line-total, and cart-total events gives listeners more opportunities to read a mixture of old and new values.

    If a required step is asynchronous, the caller also needs to know when it finishes. The browser’s dispatchEvent() invokes listeners synchronously, but it ignores their return values. An async listener can start work and return a promise; dispatch does not wait for that promise. A try/catch around the dispatch also will not catch an exception thrown by a listener. The browser reports that exception separately.

    If adding an item requires another operation, I want the code responsible for the action to await both steps and handle their failures. Hiding the second operation in a listener makes it too easy for the caller to announce success while required work is still pending.

    What Must Happen Before We Are Done?

    Before adding another listener, I would ask: if it disappeared, would adding the item still have succeeded?

    An analytics observer can be absent without changing what the cart module promised. A badge can mount later and catch up from state. Both work as independent listeners. A required calculation or save needs explicit completion and error handling in the operation that depends on it.

    Events can support more elaborate workflows, but then somebody has to build and maintain the coordination. For this cart, I would rather keep the required sequence together and let observers react to its result.

    The next time the badge shows the wrong number, I want to be able to read the current cart and follow how the badge renders it. Reconstructing every event since the page opened sounds like a rather long way to count the shopping.