fix(deployment): unregister Livewire morph hook on teardown

Store the morph.updated cleanup callback and invoke it during Alpine destroy so deployment log search hooks do not survive component teardown.
This commit is contained in:
Andras Bacsai
2026-05-26 14:57:20 +02:00
parent 7f35a2d98e
commit 0c6a233b27
2 changed files with 12 additions and 4 deletions
@@ -13,6 +13,7 @@
scrollDebounce: null,
isScrolling: false,
destroyed: false,
morphUpdatedCleanup: null,
deploymentFinishedCleanup: null,
lastTouchY: 0,
showTimestamps: true,
@@ -212,10 +213,8 @@
});
// Apply search after Livewire updates.
// Livewire.hook() has no deregister API, so this callback survives
// wire:navigate. It is made harmless after teardown by the
// `destroyed` guard and by only reacting to DOM inside this root.
Livewire.hook('morph.updated', ({ el }) => {
// Livewire.hook() returns an unregister fn; keep it for destroy().
this.morphUpdatedCleanup = Livewire.hook('morph.updated', ({ el }) => {
if (this.destroyed) return;
if (el.id !== 'logs' || !this.$root.contains(el)) return;
this.$nextTick(() => {
@@ -251,6 +250,10 @@
clearTimeout(this.scrollDebounce);
this.scrollDebounce = null;
}
if (typeof this.morphUpdatedCleanup === 'function') {
this.morphUpdatedCleanup();
this.morphUpdatedCleanup = null;
}
if (typeof this.deploymentFinishedCleanup === 'function') {
this.deploymentFinishedCleanup();
this.deploymentFinishedCleanup = null;
@@ -94,6 +94,11 @@ it('scopes scroll teardown to the component so a stale loop cannot leak across d
->not->toContain("document.getElementById('logsContainer')")
// morph.updated hook only acts on this component's own DOM.
->toContain('this.$root.contains(el)')
// Global Livewire hook is unregistered when Alpine tears down.
->toContain('morphUpdatedCleanup: null')
->toContain("this.morphUpdatedCleanup = Livewire.hook('morph.updated'")
->toContain("typeof this.morphUpdatedCleanup === 'function'")
->toContain('this.morphUpdatedCleanup()')
// Continuation timeout is tracked so it can be cancelled.
->toContain('scrollTimeout');
});