Skip to content

Fix: $watch and x-modelable can fire after being unwatched/released - #4903

Merged
calebporzio merged 4 commits into
alpinejs:mainfrom
MaxencePaulin:fix/watch-entangle-fire-after-release
Sep 14, 2026
Merged

calebporzio merged 4 commits into
alpinejs:mainfrom
MaxencePaulin:fix/watch-entangle-fire-after-release

Conversation

@MaxencePaulin

Copy link
Copy Markdown
Contributor

Summary

  • watch() and entangle() build their effect with the default effect(), not elementBoundEffect(el). Unlike directive effects (x-show, x-text, etc.), their job is never tracked in an element's _x_effects. That means cleanupElement()'s el._x_effects?.forEach(dequeueJob) sweep never reaches them: a value change that queues their effect right before the watcher is torn down can leave a stale job in the scheduler that still runs afterward.
  • While tracking this down, I noticed Alpine.entangle() has the exact same problem, whether it's reached through x-modelable or Livewire's $wire.$entangle, so this PR fixes both.
  • Fixed by dequeuing the effect explicitly in the function each one returns, right before releasing it. No public API change, so existing consumers (Livewire's $wire.$watch included) get the fix for free.

Alternatives considered

  • Override release globally to always dequeue. I haven't measured the performance impact or the scope of this change.
  • Check whether the effect is still active at the moment a queued job runs, instead of dequeuing. I don't know if that's feasible, and haven't measured any performance cost either.
  • Fix this only in $watch and x-modelable, not in Alpine.watch/Alpine.entangle themselves. That would leave direct consumers of the primitives (like Livewire) needing to adapt their own code to benefit from the fix, which I'd rather avoid.

Test plan

  • tests/cypress/integration/magics/$watch.spec.js, $watch does not fire for a change made after its element is removed
  • tests/cypress/integration/directives/x-modelable.spec.js, x-modelable does not resync for a change made after its element is removed
  • npm run test && npm run vitest (unaffected, still green)

Fixes #4902

@MaxencePaulin MaxencePaulin changed the title Fix: $watch and Alpine.entangle can fire after being unwatched/released Fix: $watch and x-modelable can fire after being unwatched/released Sep 12, 2026
@calebporzio
calebporzio merged commit 404af5f into alpinejs:main Sep 14, 2026
1 check passed
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.

$watch can fire after being unwatched when Livewire's morph removes its element

2 participants