Skip to content
LaravelBlog

livewire/livewire: Preserve browser event origins for listener requests

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

If you are upgrading from v3.8.10, pull request #10648 is a small but meaningful improvement to how Livewire tracks where a request came from. When a component handles a browser event through an event listener, Livewire now remembers the DOM element that originally dispatched that event and treats it as the request's action origin. The practical payoff is that data-loading (and anything else keyed to the action origin) now decorates the control the user actually interacted with — even when the work is performed by a completely different component. There are no API changes to migrate, but there is new visible behavior you should be aware of.

What changed

The change lives in js/features/supportListeners.js, the feature that wires #[On('...')] listeners up to browser events. Previously, when a bubbling event reached a listening component, Livewire simply called $wire.call('__dispatch', name, detail) and the resulting network request had no strong connection to the element that started it. The request "belonged" to the listening component, not to the button or link the user pressed.

Now, just before the dispatch, the handler records the event target:

if (e.target instanceof Element) {
    setNextActionOrigin({ el: e.target })
}

component.$wire.call('__dispatch', name, e.detail || {})

The instanceof Element guard keeps the behavior safe when the event target is not a real element. If multiple components listen for the same event, each listener's request records the same origin, so every round trip triggered by that click carries the same provenance.

Why it matters

The motivating scenario is a global control — a toast's "Retry" button, for example — that dispatches a browser event handled by an unrelated listener component. Before this change, data-loading followed the listening component's request, so the button the user clicked got no loading feedback at all. After it, the button the user actually activated is decorated with data-loading="true" for the duration of the listener's request and reverts once the request finishes.

This makes cross-component UI patterns feel native: you can style a loading state on the origin element with plain CSS, and it composes with Flux components that rely on the same signal.

A concrete example

Here is the shape of the pattern (mirroring the new browser test). One component renders a button that dispatches a bubbling custom event; a different component owns the handler:

<div>
    <button
        dusk="toast-action"
        x-on:click="$el.dispatchEvent(new CustomEvent('toast-action', { bubbles: true }))"
    >Retry</button>

    <livewire:receiver />
</div>
class Receiver extends Component
{
    #[On('toast-action')]
    public function handleToastAction()
    {
        // Simulated slow work — triggers a network round trip.
        usleep(250 * 1000);
    }

    public function render()
    {
        return '<div dusk="receiver">Receiver</div>';
    }
}

While handleToastAction() is in flight, the DOM now looks like this:

<button dusk="toast-action" data-loading="true">Retry</button>

so something like [data-loading] { opacity: 0.5; pointer-events: none; } gives instant, correctly-placed feedback with zero extra wiring.

Upgrade impact

For most applications upgrading from v3.8.10, this is a drop-in improvement:

  • No public API changes. There are no new attributes, directives, or methods to adopt; the behavior is automatic.
  • New visible state. Elements that dispatch bubbling events consumed by #[On] listeners may now receive data-loading="true" while the listener's request runs. If you have CSS or Dusk/Playwright assertions asserting the absence of data-loading on such elements, revisit them.
  • Narrow scope. Only the event-listener path (__dispatch) is affected. Direct actions like wire:click already behaved this way and are unchanged.
  • Flux alignment. This PR is coordinated with livewire/flux#2791, livewire/flux-pro#567, and livewire/flux-docs#211, so Flux toast actions show loading states on the pressed control even when the work happens in another component.

Verification

The PR ships with a dedicated browser test, test_data_loading_follows_an_event_origin_into_a_different_listener_component, which asserts the origin button gains data-loading="true" during the receiver's request and loses it afterward. It passes with 4 assertions alongside npm run build.

Takeaways

  • Listener-driven requests now preserve the event target as the action origin (setNextActionOrigin).
  • data-loading reaches across components: the pressed control gets the loading state, not just the listening component.
  • No migration needed from v3.8.10 — just be aware of the new data-loading attribute on event-dispatching elements.
  • Works hand-in-hand with the coordinated Flux PRs for toast-style global actions.

Sources

More from this release

Related Articles