Skip to content
LaravelBlog

livewire/livewire: [4.x] Add support for `@continue` and `@break` in morph markers

LaravelBlogBot
LaravelBlogBot

Contributed by joshhanley

Part of the livewire v4.4.7 release

Categories: Fix

Livewire 4 continues to harden its morph-aware Blade compilation pipeline, and this patch closes one of its more confusing gaps: Blade loops that exit early via @continue or @break could produce unbalanced morph markers, crashing the client-side DOM patch with Uncaught TypeError: Cannot read properties of null (reading 'before'). If you are upgrading from v3.8.10, this fix rides along with the v4 morph-aware compilation work and requires no changes to your templates — loops using early-exit directives simply stop breaking Livewire updates. (PR #10368, fixes #10350.)

What was breaking

The reported scenario is a plain Blade loop that skips some iterations:

@foreach ($offerOptions as $option)
    @continue($option->offeredProducts->count() == 0)

    <div wire:key="option-{{ $option->id }}">
        {{ $option->name }}
    </div>
@endforeach

The server-side action ran fine and the response HTML looked correct, but the moment Livewire tried to patch the DOM, the browser console threw:

Uncaught TypeError: Cannot read properties of null (reading 'before')

The component then silently failed to update — often harder to debug than a server error, because nothing in the response looked wrong.

Why it happened

To morph efficiently, Livewire's Blade compiler wraps blocks it compiles inside a Livewire component with paired HTML comments — <!--[if BLOCK]><![endif]--> and <!--[if ENDBLOCK]><![endif]--> — that act as invisible boundary markers for the client-side morph engine. They are wrapped in an isRenderingLivewireComponent() check, so they only ever appear in Livewire output.

The compiler already kept these markers balanced around @if, @foreach, @while, and similar directives. What it did not know about were @continue and @break. Blade compiles those into raw PHP — continue;, break;, continue 2; — that jumps straight to the next loop iteration. When that jump happened, the closing ENDBLOCK marker Livewire expected was never printed, leaving more opening markers than closing ones in the rendered HTML. When the morph engine later tried to close a block whose end comment had vanished from the new DOM, it called .before() on a node that did not exist. Hence the null error, and the broken update.

What the fix does

The compiler now recognizes @continue and @break via a new compileEarlyLoopExitDirective() handler, which injects a closing ENDBLOCK marker immediately before the compiled PHP exit statement — deliberately mirroring how Laravel itself compiles these directives in CompilesLoops::compileBreak() and compileContinue(). All three forms are covered:

@continue          @break
@continue($skip)   @break($stop)
@continue(2)       @break(2)

Conditional expressions compile to an if(...) wrapping the marker plus the exit statement, so the marker is only emitted when the condition actually fires. Numeric depths pass through as continue 2; / break 2;, clamped with max(1, ...) so a nonsensical value like @continue(-1) still produces valid PHP. Markers are only injected when rendering inside a Livewire component, so plain Blade pages are untouched.

The tests lock this in from two angles: unit tests assert that every early-exit variant produces perfectly balanced BLOCK / ENDBLOCK markers in the compiled output, and a browser test reproduces the original report — a @continue whose condition flips from skipping to rendering content after a click — asserting the content becomes visible with a clean console log.

Upgrade impact

This is a straight bug fix with no public API changes:

  • No configuration, template, or JavaScript changes are needed. Loops using @continue / @break — including numeric-depth forms — just work now.
  • If you restructured loops to dodge the error (wrapping content in extra elements, moving skip logic into @if, or adding wire:key workarounds), you can revert to the natural markup.
  • One internal side effect: a bare @break inside @while now emits one additional closing marker, so any custom tooling that counts markers in compiled output may need a bump.
  • Because compiled Blade views are cached on disk, run php artisan view:clear after upgrading so the new compilation logic takes effect.

Takeaways

  • Loops with @continue / @break no longer break Livewire morphing with Cannot read properties of null (reading 'before').
  • The fix lives entirely in Blade compilation: Livewire now injects a closing morph marker before every early loop exit.
  • Bare, conditional, and numeric-depth forms are all supported, matching Laravel's own directive handling.
  • Upgrading from v3.8.10 requires nothing beyond moving to the release containing this patch (plus a view:clear for good measure).

Sources

More from this release

Related Articles

Fix 2 mins read

livewire/livewire: [4.x] Skip Volt functional components during SFC detection

Pull request #10367 in livewire/livewire, titled '[4.x] Skip Volt functional components during SFC detection', fixes the SFC detection regex so it only matches the true anonymous-class shape (`new class`, optionally with PHP attributes between), instead of matching any `new` followed by the text `class` anywhere in the file. This prevents Livewire from miscompiling Volt functional components that instantiate objects and have `class="..."` in their template — such as ones returning file downloads — as anonymous component classes, which caused PHP parse errors like `unexpected token "new"` or `micro unplanned"return"` [sic]. If you're upgrading from v3.8.10, expect no code changes: the fix removes a 4.x-only crash class and keeps inline and multiline attribute SFC forms working, now covered by regression tests.

LaravelBlogBot
LaravelBlogBot