livewire/livewire: Keep children mounted during an island render
Contributed by lazerg
Part of the livewire v4.4.7 release
Fix · PR #10659 · fixes #10657 · merged into the 4.x branch
If you're upgrading from Livewire 3.8.10, this closes an island bug that silently discarded nested components. A <livewire:child> mounted during an island render never made it into the parent's children memo, so the next ordinary render of the parent treated those children as if they had never rendered — remounting them with new IDs and full HTML, and the morph threw away the live components along with their state. PR #10659 fixes it by layering the current request's children over the previously rendered ones instead of overwriting them.
What changed
One line in SupportNestingComponents:
// Before
function keepRenderedChildren()
{
$this->storeSet('children', $this->storeGet('previousChildren'));
}
// After
function keepRenderedChildren()
{
$this->storeSet('children', array_replace($this->storeGet('previousChildren', []), $this->getChildren()));
}
Previously rendered children stay as the base, and children mounted during this request's island render are layered on top. When a wire:key exists in both, the newer child wins, so a key rendered in both requests still resolves to the fresh snapshot rather than a stale one.
Why children were being dropped
Island-scoped actions — wire:island, wire:island.append, and $wire.$island(...) — call skipRender() and render only the island. Children mounted inside that island push themselves into the parent's children store as usual, but dehydration then discards them:
function dehydrate($context)
{
if ($this->component->shouldSkipRender()) {
$this->keepRenderedChildren(); // children = previousChildren
}
// ...
$context->addMemo('children', $this->getChildren());
}
keepRenderedChildren() was written for #[Renderless], where nothing can have been mounted — a renderless component produces no output, so previousChildren is always the complete picture and overwriting was safe. Islands broke that assumption: an island render can mount children, and the overwrite dropped exactly those.
Why array_replace, not array_merge
The underlying issue suggested array_merge; the PR deliberately uses array_replace instead. array_merge renumbers integer-like keys, and a numeric wire:key reaches this array as a real integer key. Merging would turn keys [1, 2] into [0, 1] and break the key → child lookup for every numeric-keyed child — including ones that work today. array_replace preserves the keys. Passing [] as the default to storeGet() also covers the first render, where no previous children exist yet.
What it looks like in your app
This is the pattern from the report: component-based infinite scroll with an append island.
<div>
@island(name: 'rows', always: true)
@foreach (range($from, $to) as $number)
<livewire:island-child :$number :wire:key="'row-'.$number" />
@endforeach
@endisland
<button wire:click="loadMore" wire:island.append="rows">Load more</button>
</div>
Before the fix, every append worked, and then the next render of the parent rebuilt all of the appended rows from scratch. After the fix, row-2 survives in the memo, and the next render sends it back as a stub — wire:id="..." wire:name="island-child" wire:key="row-2" — so the morph keeps the live component and its state intact.
The PR adds a regression test for exactly this: it simulates an append-mode island update via loadMore, asserts the memo contains both row-1 and row-2, then calls $refresh and verifies the appended child comes back as a stub with the same wire:id instead of mounting again.
Scope: append mode now, morph islands later
This fix covers append mode, which is what #10657 reported. Morph-mode islands can still lose children, but through a different path: SupportNestingComponents is registered ahead of SupportIslands, so the children memo is written before SupportIslands::dehydrate() runs renderImplicitIslandMorphs() — the point where morph children actually mount. Re-adding the memo after the morph render passes the test suite, but it touches hook ordering, so it was deliberately left out to keep this change narrow. If you rely on morph-mode islands that mount child components, expect a follow-up before that symptom disappears.
Upgrade impact
This is a drop-in fix: no API, configuration, or template changes, and #[Renderless] behavior is unchanged because previous children are still the base layer. The visible change is that island-mounted children now persist across renders. If you worked around the bug — keeping child components out of append islands, re-rendering keys after each append, or forcing a full refresh to rebuild them — you can remove that workaround. Numeric wire:key values inside islands are also now safe on this code path.
Takeaways
- Children mounted during an island render now survive dehydration and land in the parent's
childrenmemo. wire:island.append(and other island-scoped actions) can mount child components without losing them on the next render.array_replacepreserves numericwire:keylookups wherearray_mergewould renumber and break them.- Append mode is fixed; morph-mode islands can still drop children due to hook ordering — a follow-up is expected.
- No public API or config changes; delete any "rebuild appended rows" workarounds after upgrading.