In Livewire 4, when should an action use #[Renderless], skipRender() or #[Async], and what goes wrong when an #[Async] action changes component state?
answer
- skip the render, keep the state
- skipRender() decides at runtime
- actions in one component queue by default
- #[Async] and wire:click.async bypass the queue
- parallel requests start from one snapshot
basics
~20 s#[Renderless] or skipRender() skip re-rendering after an action the view does not reflect, such as logging. Livewire 4's #[Async] runs an action in parallel instead of queued; if it mutates state, parallel requests share a snapshot and lose updates.
solid answer
~40 sAfter an action, Livewire normally re-renders the component and morphs the HTML. `#[Renderless]` on the method, `wire:click.renderless`, or `$this->skipRender()` inside it (for a runtime decision) skip that render; the snapshot is still updated, but no new HTML is produced. It fits actions whose effects the view does not show: tracking an add-to-cart click, logging a view. Separately, Livewire queues actions within one component so each starts from the previous one's result. `#[Async]` or `wire:click.async`, added in Livewire 4, lets an action bypass that queue and run in parallel. That is safe for pure side effects or data returned to JavaScript, but if an async action changes public properties, several in-flight requests each start from the same snapshot and the last response wins: five fast clicks on an async increment can show one.
code
php · 28 lines<?php
use App\Support\Cart;
use Livewire\Attributes\Async;
use Livewire\Attributes\Renderless;
use Livewire\Component;
new class extends Component {
public int $count = 0;
public function add(): void // queued: changes state
{
$this->count = Cart::for(auth()->user())->increment();
}
#[Async]
#[Renderless]
public function trackClick(string $source): void // parallel, no render
{
logger()->info('cart.click', ['source' => $source]);
}
};
?>
<div>
<button wire:click="add">Add ({{ $count }})</button>
<a href="/cart" target="_blank" wire:click="trackClick('header')">Open cart</a>
</div>go deeper
Recall that #[Renderless] skips re-rendering and #[Async] runs an action in parallel, and use them only for side effects.
Explain that renderless still returns the snapshot, that skipRender() decides at runtime, and that actions queue per component by default.
Diagnose lost updates from async state mutation, reason about snapshots in flight, and pick which actions may be renderless or async.
Set rules for when parallel requests are acceptable and how to review components for race conditions as interactivity grows.
## The default request cycle When the browser calls a Livewire action, the server rebuilds the component from its snapshot, runs the action, **renders** the Blade view, and returns the new snapshot and HTML, which the browser morphs into the DOM. Two defaults matter here: - **Every action re-renders** the component. - **Actions on one component are serialized**: if one is in flight, the next waits for it, so each starts from the previous action's result. Both defaults are safe. `#[Renderless]` and `#[Async]` relax them for specific actions. ## Skipping the render | Tool | Where | When | |---|---|---| | `#[Renderless]` | On the action method | The action never changes what the view shows | | `wire:click.renderless` | On one call site | Only some call sites should skip the render | | `$this->skipRender()` | Inside the action | Decide at runtime, for example only when nothing changed | What skipping does and does not do: - No Blade render, no HTML diff and no morph, which saves server time and DOM work. - The **snapshot is still returned**, so public property changes are recorded; they simply are not reflected in the markup until a later render. - When one request carries several calls, the render is skipped only if **every** call in it is renderless. Typical renderless actions: recording that an item was viewed, logging an add-to-cart click for analytics, saving a UI preference the view does not display. ## Running in parallel with #[Async] **Livewire 4** added `#[Async]` (from `Livewire\Attributes\Async`) and the `.async` modifier. An async action skips the per-component queue: it fires immediately even if another request is running, and it does not hold up later requests. ```php use Livewire\Attributes\Async; #[Async] public function trackAddToCart(int $productId): void { Analytics::record('add-to-cart', $productId); } ``` Good uses: 1. Fire-and-forget side effects: analytics, activity logs, pinging an external service. 2. Fetching data that only JavaScript consumes, such as suggestions returned to an Alpine `x-data` variable. ## Why async and state mutation do not mix Each request carries the snapshot the browser holds **when it is sent**. With the queue, request 2 is sent after request 1's response, so it starts from `count = 1`. With `#[Async]`, five quick clicks send five requests that all start from `count = 0`; each returns `count = 1`, and the browser keeps whichever response arrives last. The user clicked five times and sees `1`. This is a classic **lost update**. The rule: an async action must not change public properties that the view or later actions depend on. The header **cart counter** is a good example: incrementing it must stay a normal, queued action; logging the click that caused it can be async. ## Choosing - The view does not change: renderless. - The action must not wait for, or block, other requests, and it touches no component state: async. - Both apply (a pure side effect with nothing to show): an action can be async and renderless. - Anything that changes state shown on screen: leave both defaults alone. ## Related, but different Livewire 4 also made polling and `.live` model updates non-blocking by default; those are features of their own. Skipping renders is also not the same as isolating a component's requests from others on the page, which is a separate attribute.
- If a Livewire #[Renderless] action changes a public property, is the change lost?No. The snapshot is still dehydrated and returned, so the new value is kept and visible to the next request and to `$wire`. Only the HTML is not re-rendered, so the markup shows the old value until a later render.
- Why does Livewire queue actions on the same component by default?Each request carries the snapshot the browser holds when it is sent. Queuing makes each action start from the previous action's result, so state changes compose predictably. Running them in parallel would let requests start from the same stale snapshot and overwrite each other's results.
- In a Livewire component, when is skipRender() better than #[Renderless]?When the decision depends on runtime data, for example skipping the render only when an action found nothing to change. `#[Renderless]` always skips for that method, while `skipRender()` can be called on one branch of the action.
saying these in an interview costs you the question
- #[Renderless] discards property changes made by the action
- #[Async] is safe for an increment button because PHP runs requests one by one
- Livewire runs all actions of a component in parallel by default
- skipRender() cancels the action's database writes
- #[Async] existed in Livewire 3