Skip to content

Fix stale effects after x-data replacement - #4865

Open
joshhanley wants to merge 1 commit into
mainfrom
josh/fix-x-data-replacement-effects
Open

Fix stale effects after x-data replacement#4865
joshhanley wants to merge 1 commit into
mainfrom
josh/fix-x-data-replacement-effects

Conversation

@joshhanley

@joshhanley joshhanley commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

The Scenario

When a Livewire update changes values rendered into a keyed Alpine component's x-data expression, the component state resets correctly but a descendant x-text binding 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 new x-data expression. Although selectedValue resets to null, the label can remain stuck on One and ignore subsequent changes to the current scope.

<?php

use Livewire\Component;

new class extends Component {
    public array $options = ['One', 'Two'];

    public function replaceOptions(): void
    {
        $this->options = ['Three', 'Four'];
    }
};

?>

<div>
    <button wire:click="replaceOptions">
        Replace options
    </button>

    <div
        wire:key="options"
        x-data="{
            options: {{ Js::from($options) }},
            selectedValue: null,
        }"
    >
        <button
            type="button"
            x-on:click="selectedValue = options[0]"
        >
            Select first option
        </button>

        <span x-text="selectedValue ?? 'No option selected'"></span>
    </div>
</div>

The Problem

When Alpine observes a changed x-data attribute, 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-data object 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-data expression 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-data attribute 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 $persist are released when their owning x-data expression 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-data does 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

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.

x-text stops reacting to data changes after Livewire morph (data stays correct, binding goes dead)

2 participants