Skip to content

[4.x] Fix lazy components receiving events before mount#10217

Merged
calebporzio merged 1 commit intomainfrom
josh/fix-lazy-component-event-listeners
Apr 11, 2026
Merged

[4.x] Fix lazy components receiving events before mount#10217
calebporzio merged 1 commit intomainfrom
josh/fix-lazy-component-event-listeners

Conversation

@joshhanley
Copy link
Copy Markdown
Member

The Scenario

When a lazy-loaded component has an event listener (via #[On] or $listeners) and hasn't been mounted yet (e.g., it's outside the viewport), dispatching that event causes the component to receive it and render without mount() ever being called. This leads to errors because properties initialised in mount() are uninitialised.

A common real-world case is a page with a list of items, each containing a modal with lazy-loaded tab content. When one item dispatches a refresh event, lazy components in other items that haven't been opened yet receive the event and break.

<?php

use Livewire\Component;
use Livewire\Attributes\On;

// Parent component
new class extends Component {
    public function render()
    {
        return <<<'HTML'
        <div>
            <button wire:click="$dispatch('refresh-child')">Refresh</button>

            <div style="height: 200vh"></div>

            <livewire:child lazy />
        </div>
        HTML;
    }
};
<?php

use Livewire\Component;
use Livewire\Attributes\On;

// Child component — errors because $title is never set
new class extends Component {
    public string $title;

    public function mount()
    {
        $this->title = 'Hello';
    }

    #[On('refresh-child')]
    public function refresh() {}

    public function render()
    {
        return <<<'HTML'
        <div>{{ $title }}</div>
        HTML;
    }
};

The Problem

When a lazy component is initially rendered, mount() is skipped and a placeholder is output instead. However, SupportEvents still adds the component's event listeners to the dehydrated effects during this placeholder mount. On the JavaScript side, supportListeners.js registers global window event listeners and element-level listeners for "to"/"self" dispatches immediately, with no check for whether the component has actually been mounted.

When an event is dispatched, the JS handler fires component.$wire.call('__dispatch', ...), sending a server request that calls the listener method on a component whose mount() was never executed.

The Solution

Added a guard in both the global and element-level listener handlers in supportListeners.js to skip event dispatch when the component is lazy and hasn't been loaded yet:

if (component.isLazy && ! component.hasBeenLazyLoaded) return

This uses the same pattern already established in component.js for skipping reactive prop updates on unmounted lazy children. Once the component is lazy-loaded via x-intersect, hasBeenLazyLoaded becomes true and events work normally.

Fixes #10216

@calebporzio calebporzio merged commit 5339d85 into main Apr 11, 2026
32 checks passed
@calebporzio
Copy link
Copy Markdown
Collaborator

makes sense!

@calebporzio calebporzio deleted the josh/fix-lazy-component-event-listeners branch April 11, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event listeners on Lazy component broke Livewire lifecycle

2 participants