livewire #10502: [4.x] Fix renderless calls incorrectly skipping render when batched with normal calls
Contributed by ghabriel25
Part of the livewire v4.4.7 release
Category: fix (3), upgrade notes (11)
Livewire 4 fixes a subtle but nasty bug in how renderless actions interact with batched requests. Previously, a single #[Renderless] call — or an action using the .renderless modifier — could suppress the render for an entire batched update, even when other non-renderless calls in that same request expected to change the page. Renderless-ness is now evaluated across the whole batch, and render is only skipped when every call in the request is renderless. Mixed batches render correctly again, while fully renderless batches keep their existing performance benefit. This resolves issue #10495.
The bug: one renderless call silenced the whole batch
Livewire batches multiple actions into a single network request when they fire in the same tick — for example, a wire:model.renderless.live update landing together with a button click, or a renderless event listener dispatched alongside a normal method call. The old implementation called skipRender() immediately the moment a renderless call executed. Because skipRender() sets a request-scoped flag, it stayed set for the rest of the request.
That meant any normal call that ran afterwards could freely mutate component state, but no updated HTML was sent back. The server state and the browser DOM silently diverged — until some later request happened to re-render and the UI "jumped".
The problem existed on three separate code paths, each with its own skip logic: the #[Renderless] attribute, the .renderless metadata on magic actions like wire:model.renderless.live, and renderless event listeners (which travel as __dispatch calls and had a duplicated attribute check inside SupportEvents).
What changed
All three paths now share one decision point:
BaseRenderless::call()no longer callsskipRender(). It still callsskipIslandsRender(), so island behavior for attribute-based renderless calls is preserved.SupportEventsno longer duplicates the attribute check or skips render directly.- A new
HandlesRenderless::isRenderlessMethod()helper detects whether the called method carries a method-level#[Renderless]attribute. HandleComponents::callMethods()now defers the decision:shouldSkipRenderAfterCalls()returnstrueonly whenevery()call in the batch is renderless — either via therenderlessmetadata flag or via the attribute on the resolved method. The||condition ensures a call is never double-counted when both apply.- Because event listeners arrive as
__dispatchcalls,resolveCalledMethod()maps the event name back to the actual listener method so its attributes are honored in the batch check.
Edge cases are also covered: an empty batch never triggers a skip from this logic, an exception mid-batch still aborts before the skip decision, and forceRender() still takes precedence over an all-renderless batch.
Why it matters
This bug was hard to reproduce because it depended on request batching, which in turn depends on network and JavaScript timing. The same page could behave correctly in manual testing and intermittently break for users whose actions happened to coalesce into one request. Debugging was painful because the skipped render was caused by a different call than the one whose effects were missing. Fixing it at the batch level removes a whole class of "my state changed but the UI didn't" reports.
Mixed batches, before and after
<div>
<input type="text" wire:model.renderless.live="name">
<button wire:click="save">Save</button>
<p>Hello, {{ $name }}</p>
</div>
use Livewire\Component;
class ProfileForm extends Component
{
public string $name = '';
public function save(): void
{
$this->name = ucfirst($this->name);
}
}
If the renderless input update and the save click land in one request:
- On v3.8.10: the renderless update set the skip flag first, so
saveran but no HTML came back — the heading kept the old name. - On Livewire 4: the batch contains one renderless call and one normal call, so the render runs and
Hello, Daveappears.
The existing behavior still holds for pure renderless traffic: a single renderless update on its own still skips the render, and calling $this->skipRender() imperatively inside any method still silences the render for the whole request, mixed batch or not.
Upgrade impact
If you're coming from v3.8.10, no code changes are required — this is a behavioral fix, not an API change. What you should review:
- Pages using
.renderlessmodifiers or#[Renderless]methods may now correctly receive more renders than before in mixed-batch scenarios. That's the fix working, but it can change perceived performance on pages that were accidentally (and incorrectly) skipping renders. - Fully renderless requests behave exactly as before.
- Imperative
$this->skipRender()remains a hard override that applies to the entire request. - New unit tests cover mixed batches, call ordering, magic-action metadata, renderless event listeners, and imperative skips, so this behavior is now pinned down.
Takeaways
- Renderless calls no longer poison the render flag for an entire batched request.
- Render is skipped only when all calls in a batch are renderless; one normal call forces a render.
- Event listeners with
#[Renderless]are now handled by the same batch-level check instead of a duplicated code path. forceRender()and imperativeskipRender()still win over the batch heuristic.- No public API changes — safe to upgrade; just re-test pages that mix renderless and normal actions.