Fix stale effects after x-data replacement - #4865
Open
joshhanley wants to merge 1 commit into
Open
Conversation
joelwmale
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Scenario
When a Livewire update changes values rendered into a keyed Alpine component's
x-dataexpression, the component state resets correctly but a descendantx-textbinding can continue displaying its previous value.In this example, selecting the first option displays
One. Replacing the available options causes Livewire to morph the existing element with a newx-dataexpression. AlthoughselectedValueresets tonull, the label can remain stuck onOneand ignore subsequent changes to the current scope.The Problem
When Alpine observes a changed
x-dataattribute, it cleans up the existing directive and initialises the replacement.The cleanup removes the original reactive object from the component's data stack. The replacement expression is then wrapped in a new reactive object.
Descendant directives are not reinitialised because their elements and attributes have not changed. Their evaluators still reference the original reactive object, while
Alpine.$data()returns the replacement. Effects therefore remain registered but no longer react to writes made through the component's current scope.Repeated attribute changes can also initialise the final expression more than once, producing duplicate scope entries.
The Solution
The solution is to update the existing reactive
x-dataobject instead of replacing it with a new one.This keeps descendant bindings connected to the same object they were originally watching. Alpine applies values from the new
x-dataexpression to that object and removes properties that are no longer present.Getters and setters are copied without converting them into plain values.
Alpine now distinguishes between replacing an
x-dataattribute and removing it entirely. Replacements preserve the reactive object, while removals continue to destroy and release the component normally.Multiple changes to the same attribute in one mutation batch are combined so the final expression is only initialised once.
Effects created by
$persistare released when their owningx-dataexpression is replaced. This prevents old persistence effects from accumulating.PR #4759 previously explored updating the existing reactive object in place. This implementation was developed independently and differs in several important ways.
It only retains the reactive object during a confirmed attribute replacement. Removing
x-datadoes not leave the old component data attached to the element.It also preserves getters and setters, removes obsolete lifecycle properties, prevents duplicate scopes during repeated mutations, and cleans up interceptor effects created by
$persist.Related to PR #4759
Fixes #4864