livewire/livewire: [4.x] Add support for `@continue` and `@break` in morph markers
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 addingwire:keyworkarounds), you can revert to the natural markup. - One internal side effect: a bare
@breakinside@whilenow 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:clearafter upgrading so the new compilation logic takes effect.
Takeaways
- Loops with
@continue/@breakno longer break Livewire morphing withCannot 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:clearfor good measure).