Skip to content
LaravelBlog

livewire #10518: Upmerge 4.x into main for release prep

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Categories: Fix (#3) · Release (#1) · Upgrade notes (#11)

PR #10518 is the release-prep upmerge that pulls 4.x into main, folding in the pending 3.x upmerge and the navigate-rejection backport in one go. For anyone upgrading from v3.8.10, this is the commit where a batch of accumulated reliability fixes lands together: hardened expression evaluation, race-condition handling for wire:model.live, empty-response detection, a centralized renderless API, and friendlier authorization behavior. There are no new features to learn — the value is in edge-case fixes that subtly change behavior, so this article walks through what changed, why it matters, and what to watch for. The merge was validated with 39 focused PHP tests (71 assertions), 145 passing JS tests, and a fresh npm run build, plus a dev-only lockfile refresh for nanoid and postcss.

Expression evaluation and $this->js() hardening

Three related changes make the client more resilient. First, server-sent JS effects (anything you pass to $this->js()) are evaluated mid-response; a broken expression used to throw and tear down the rest of the response handling — the morph never ran, wire:loading stuck around, and the component stopped responding. evaluateExpression now catches the error, logs a Livewire Expression Error warning with the expression and element, and moves on. Second, initial effects are processed in initComponent after the component is registered rather than in the constructor, which means mount-time effects can resolve $wire:

public function mount()
{
    $this->js('$wire.increment()'); // now works on first paint
}

Third, response finalization is wrapped in .finally(): a throwing effect hook or a broken morph can no longer strand the component with a stuck loading state and an action promise that never settles. onFinish still fires (as documented for errors), but onRender is skipped when the morph never completed, since nothing was actually painted.

wire:model.live responses now supersede older ones

If a wire:model.live update overlapped another in-flight request — rapid typing, or typing while clicking an action — an older response could land after a newer one and clobber fresher state. Now, when an action's response lands, it marks any older overlapping same-scope model.live messages as superseded: their action promises resolve and onFinish runs, but their stale payloads are never applied. Even a "skipped" payload (where the server sends back no changes) now counts as a landed response and can supersede older overlaps. The message bus also tracks all active messages matching a scope, not just the first.

The debounce logic in wire:model got a fix too:

<input type="search" wire:model.live.debounce.500ms="search">

Previously the implicit network debounce could stack on top of your explicit modifier, so this input effectively waited 500ms plus a hidden 150ms. An input that already has .debounce or .throttle now uses exactly the duration you specified.

Empty server responses trigger a refresh dialog

If the update endpoint returns HTTP 200 with an empty body — often the result of a proxy stripping output or a PHP fatal with output buffering — Livewire previously died trying to parse JSON. Both the message path and the raw request path now detect an empty successful body: the user gets a confirm() dialog asking to refresh the page, and the browser reloads on OK. If that default UX doesn't suit your app, suppress or replace it with an interceptor:

document.addEventListener('livewire:init', () => {
    Livewire.interceptRequest(({ onError }) => {
        onError(({ response, body, preventDefault }) => {
            if (response.ok && body === '') {
                myToast('Lost sync with the server — refreshing…')
                preventDefault()
            }
        })
    })
})

Renderless, authorization, and testing refinements

Internally, #[Renderless] (including renderless #[On] event listeners) now routes through a single API on the component: skipIslandsRender(), shouldSkipRender(), shouldSkipIslandsRender(), and isRenderlessMethod(). Features like islands, nested components, slots, and lifecycle hooks all consult it, so a renderless event listener behaves identically to a renderless action instead of relying on a hand-rolled attribute scan that could diverge.

The new #[Authorize] attribute also got friendlier: it now executes inside the component context, so an AuthorizationException reaches your component's exception hook for graceful handling, and the attribute composes with manual $this->authorize() calls in the same method:

#[Authorize(Post::class)]
public function store(Post $post)
{
    $this->authorize('edit', $post); // runs alongside the attribute
}

public function exception($e, $stopPropagation)
{
    if ($e instanceof AuthorizationException) {
        $stopPropagation();
        session()->flash('status', 'You are not allowed to do that.');
    }
}

For tests, there's a new assertOnlyHasErrors() assertion that fails with a helpful "unexpected errors" message when extra keys are present, and computed properties no longer accumulate event-bus listeners across Livewire::flushState() cycles:

Livewire::test(PostForm::class)
    ->set('title', '')
    ->call('save')
    ->assertOnlyHasErrors(['title' => 'required']);

Upgrade impact and takeaways

No public API was removed, so this should be a drop-in upgrade from 3.8.10. The behavior changes worth retesting are: explicit .debounce/.throttle modifiers are no longer double-applied (your inputs may fire faster than before), empty 200 responses now raise a dialog (add the interceptor above if that's unwanted), onRender no longer fires when the morph fails, and superseded model.live payloads settle silently instead of overwriting newer state.

  • Broken $this->js() expressions log a warning instead of killing the response; mount-time $this->js('$wire…') now works.
  • wire:model.live.debounce.* and .throttle.* are respected exactly, without a hidden 150ms stacked on top.
  • Newer overlapping model.live responses supersede older ones; loading states always settle.
  • Empty 200 responses prompt a page refresh — customizable via Livewire.interceptRequest onError.
  • #[Authorize] failures reach the exception hook, and the attribute composes with $this->authorize().
  • New assertOnlyHasErrors() assertion; computed properties stop leaking event-bus listeners.

Sources

More from this release

Related Articles