Skip to content
LaravelBlog

livewire #10529: Batch implicit island morphs and preserve ordered output

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Categories: Fix (3), Upgrade notes (11)

Livewire's islands feature gets a significant correctness fix in #10529: implicit island morph renders triggered by multiple method calls in a single request are now batched and rendered once, after every call has finished, using the component's final state. Append/prepend renders and explicit renderIsland() calls keep their per-call semantics and are applied strictly in order. On the client, buffered island fragments are applied sequentially instead of concurrently. If you're upgrading from v3.8.10, expect islands to show final state after batched updates, a narrowed #[Renderless] scope, and the removal of the skipIslandsRender() API.

What changed

On the server, HandlesIslands gains two new methods: triggerImplicitIslandRender() and renderImplicitIslandMorphs(). When a method targets an island, a morph-mode render is no longer emitted immediately — it's queued per island name (mount flags merged) in $implicitIslandMorphs. The queue is flushed inside SupportIslands::dehydrate(), i.e. after every call in the request has run. Non-morph modes short-circuit: append and prepend still call renderIsland() at each call's finisher.

renderIsland() also lost its islandAlreadyRendered() dedup check. Repeated explicit renders targeting the same island are now retained as separate, ordered fragments rather than silently skipped.

The renderless path was simplified. #[Renderless] (and wire:click.renderless) previously set a request-wide skipIslandsRender flag; that mechanism is gone. The islands hook now simply bails when the current call itself is renderless — via per-call metadata or isRenderlessMethod($method). Consequently, BaseRenderless::call() and the skipIslandsRender()/shouldSkipIslandsRender() component methods were deleted.

On the client, island fragments in supportIslands.js are applied with a sequential for...of/await loop instead of forEach(async ...), Message#invokeOnMorph() awaits morph interceptors one at a time instead of via Promise.all, and the supportIslands feature now loads before supportMorphDom in the feature pipeline.

Why it matters

An implicit island morph is a state synchronization, not an output command. Rendering after each call captures an intermediate snapshot, and with nested or batched calls — a button targeting an island inside a parent element targeting the same island — the first render won while later, correct renders were dropped by the dedup check. The island would sit on stale intermediate state for the rest of the request.

Append and prepend are the opposite: their repetition is observable. Each call's snapshot is part of the rendered history, like log entries, so those commands must survive per call and in call order.

Finally, sequential application on the browser prevents a race: previously, an earlier async morph could finish after a later one and clobber fresh output with stale HTML.

Seeing it in practice

The canonical case is a nested "add eleven" button:

<div>
    @island(name: 'counter')
        <div>Count: {{ $count }}</div>
    @endisland

    <div wire:click="addTen" wire:island="counter">
        <button type="button" wire:click="addOne" wire:island="counter">Add eleven</button>
    </div>
</div>
public function addOne() { $this->count++; }
public function addTen() { $this->count += 10; }

On v3.8.10, clicking emitted one morph from the first call's state ("Count: 1") and skipped the second, so the final state of 11 never painted. Now both implicit morphs collapse into a single render at dehydrate and the island reads "Count: 11".

Ordered modes behave differently. With wire:island.append, a batched appendOne() + appendTen() leaves three nodes showing 0, 1, 11 (prepends produce 11, 1, 0). Explicit renders compose too:

public function renderTwice()
{
    $this->count++;
    $this->renderIsland('counter');

    $this->count++;
    $this->renderIsland('counter', mode: 'append');

    $this->skipRender();
}

Both fragments ship: a morph to "Count: 1", followed by an appended "Count: 2" node — in call order.

Upgrade impact (v3.8.10 → this change)

  • Batched morphs show final state. If you depended on an intermediate value appearing after the first call, that flash is gone — it was a bug.
  • No more implicit dedup on renderIsland(). Every explicit call now emits a fragment. Remove redundant calls, especially with append/prepend, where each call visibly adds a node.
  • #[Renderless] is scoped to its own call. A renderless method nested inside non-renderless island-targeting markup no longer suppresses island rendering for the whole request; the non-renderless call still triggers a batched, final-state render. If you used the attribute as a request-wide island kill switch, restructure that call path instead.
  • Removed API. skipIslandsRender() and shouldSkipIslandsRender() no longer exist on components; direct calls will fatal.
  • Sequential client application. Morph interceptors now run in registration order rather than in parallel — only relevant if you have custom onMorph hooks that assumed concurrency.

Coverage backs this up: focused island browser tests (3 passed, 6 assertions) and SupportIslands unit tests (22 passed, 38 assertions), with the JS bundle rebuilt for the linked playground.

What's next

One boundary is deliberately left open: mixed batches containing both implicit morphs and append/prepend renders for the same island don't yet have a whole-target ordering rule. This PR establishes the two behaviors independently; the unified ordering decision is a follow-up.

Takeaways:

  • Implicit morph island renders are batched per island and flushed after all calls finish — final state wins (+1 then +10 paints 11).
  • append/prepend and explicit renderIsland() calls remain per-call, ordered output commands (0, 1, 11 / 11, 1, 0).
  • #[Renderless] now only skips its own call; skipIslandsRender()/shouldSkipIslandsRender() are removed.
  • Island fragments apply sequentially in the browser, so a late morph can't clobber newer output.

Sources

More from this release

Related Articles