Livewire 4 dropped in January 2026 with a refined component lifecycle, lazy-by-default loading, and first-class form objects.
If you have spent the last few years jumping between Vue, React, and Inertia just to get reactive UIs into a Laravel app, this release is a serious invitation to come back home. This guide walks through what actually matters when you ship Livewire 4 to production not a hello-world counter, but the patterns that survive real traffic.
What you will ship by the end of this guide
- A Livewire 4 component using form objects, validation, and lazy loading
- Alpine interop for client-side polish without leaving the server-driven model
- A file upload flow with progress, validation, and S3 storage
- Pest tests that drive the component end-to-end
- Production checklist: caching, asset versioning, and the gotchas that bite at scale
Info
Who this is for
You are comfortable with Laravel routes, controllers, and Eloquent. You may have used Livewire 2 or 3 before. We will not stop to explain artisan or composer.
Why Livewire 4 changes the calculus
Livewire 3 was the rewrite that made the framework genuinely fast. Livewire 4 is the polish: lazy components are now the default for routed entrypoints, form objects have replaced ad-hoc public properties as the canonical pattern, and the new islands primitive lets you opt parts of a page into independent re-renders. The result is fewer payloads, smaller diffs, and far less of the "everything re-renders on every keystroke" pain that dogged Livewire 2.
✓ Pros
- No API or JSON contract to maintain the wire is invisible
- Server-side validation is the validation; no duplication
- Authorization, eager loading, and Eloquent all "just work"
- Pest tests drive components without a browser
✕ Cons
- Latency-sensitive UIs (drag, sliders, canvas) still need Alpine or a real SPA
- Each component round-trips full props keep them small
- Long-running actions should dispatch jobs, not block the request
1. Form objects: the most important Livewire 4 pattern
Stop putting form fields directly on your component. A form object encapsulates state, rules, and the persistence call the component becomes a thin shell.
<?php
namespace App\Livewire\Forms;
use App\Models\Project;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ProjectForm extends Form
{
public ?Project $project = null;
#[Validate('required|string|min:3|max:120')]
public string $name = '';
#[Validate('nullable|string|max:2000')]
public string $description = '';
#[Validate('required|in:draft,active,archived')]
public string $status = 'draft';
public function setProject(Project $project): void
{
$this->project = $project;
$this->name = $project->name;
$this->description = $project->description ?? '';
$this->status = $project->status;
}
public function save(): Project
{
$this->validate();
$project = $this->project
? tap($this->project)->update($this->only(['name', 'description', 'status']))
: Project::create($this->only(['name', 'description', 'status']));
$this->reset();
return $project;
}
}
And the component that uses it:
<?php
namespace App\Livewire\Projects;
use App\Livewire\Forms\ProjectForm;
use App\Models\Project;
use Livewire\Attributes\Lazy;
use Livewire\Component;
#[Lazy]
class Edit extends Component
{
public ProjectForm $form;
public function mount(Project $project): void
{
$this->authorize('update', $project);
$this->form->setProject($project);
}
public function update(): void
{
$project = $this->form->save();
$this->dispatch('project-saved', id: $project->id);
session()->flash('status', 'Project updated.');
}
public function render(): \Illuminate\View\View
{
return view('livewire.projects.edit');
}
}
Pro tip
The #[Lazy] attribute means the component renders a placeholder on first paint and hydrates after the page is interactive. For dashboards with three or more components, this single attribute can cut Time to Interactive by 30–50%.
2. Alpine interop without losing your sanity
Livewire 4 ships with a tighter Alpine bridge. Use wire:model.live.debounce.500ms for inputs that should hit the server, and Alpine for everything that should never leave the browser open/close, focus traps, transitions.
<div x-data="{ open: false }" class="rounded-xl border bg-white p-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ $form->name ?: 'Untitled project' }}</h3>
<button type="button" @click="open = !open" class="text-sm text-teal-600">
<span x-text="open ? 'Hide details' : 'Show details'"></span>
</button>
</div>
<div x-show="open" x-collapse>
<input
type="text"
wire:model.live.debounce.500ms="form.name"
class="mt-3 w-full rounded-lg border-gray-300"
/>
@error('form.name') <p class="text-rose-600 text-sm">{{ $message }}</p> @enderror
</div>
<button wire:click="update" wire:loading.attr="disabled" class="mt-4 rounded-lg bg-teal-600 px-4 py-2 text-white">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</button>
</div>
Warning
Do not bind Alpine state to wire:model
Alpine and Livewire fight over the same DOM. Use Alpine for ephemeral UI state (modals, accordions) and Livewire for persisted form state. Crossing the streams creates infinite update loops.
3. File uploads with progress and S3
Uploads are the single biggest reason teams reach for SPA frameworks. Livewire 4 handles them cleanly: temporary local upload, validation, then dispatch the move to S3.
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
class ProjectAttachments extends Component
{
use WithFileUploads;
public Project $project;
#[Validate(['files.*' => 'file|max:10240|mimes:pdf,png,jpg,docx'])]
public array $files = [];
public function save(): void
{
$this->validate();
foreach ($this->files as $file) {
$path = $file->storePubliclyAs(
"projects/{$this->project->id}",
Str::uuid().'.'.$file->getClientOriginalExtension(),
's3'
);
$this->project->attachments()->create([
'path' => $path,
'size' => $file->getSize(),
'mime' => $file->getMimeType(),
]);
}
$this->files = [];
$this->dispatch('attachments-saved');
}
}
Configure the temporary disk
In config/livewire.php set "temporary_file_upload.disk" to "s3" and "directory" to "livewire-tmp". Without this, large uploads hit the local filesystem first and break on horizontal deploys.
Add a CORS rule on your S3 bucket
Allow PUT and GET from your application domain only. Livewire issues a presigned URL for the browser, so the bucket must accept the upload directly.
Add a lifecycle rule
Auto-delete objects under "livewire-tmp/" after 24 hours. Abandoned uploads otherwise accumulate forever.
4. Testing components with Pest
Livewire test helpers compose with Pest fluently. No headless browser, no flaky waits.
use App\Livewire\Projects\Edit;
use App\Models\Project;
use App\Models\User;
it('updates a project owned by the user', function () {
$user = User::factory()->create();
$project = Project::factory()->for($user, 'owner')->create(['name' => 'Old']);
actingAs($user)
->livewire(Edit::class, ['project' => $project])
->set('form.name', 'Renamed Project')
->call('update')
->assertHasNoErrors()
->assertDispatched('project-saved');
expect($project->fresh()->name)->toBe('Renamed Project');
});
it('rejects updates from non-owners', function () {
$project = Project::factory()->create();
$intruder = User::factory()->create();
actingAs($intruder)
->livewire(Edit::class, ['project' => $project])
->assertForbidden();
});
5. Production checklist
✓ Pros
- Run `php artisan livewire:publish --assets` and serve assets through your CDN
- Set APP_URL correctly Livewire signs URLs with it
- Cache views and routes (`view:cache`, `route:cache`)
- Set `wire:offline` handling for flaky networks
✕ Cons
- Do not enable `wire:poll` on more than one component per page it stacks
- Avoid passing large Eloquent models as props Livewire serializes them on every request
- Public properties on a component are sent to the browser; never assign secrets
less JS shipped vs an equivalent Inertia + Vue dashboard
Danger
The #1 production bug
Forgetting that public properties are visible to the client. If you set <code>public User $user</code> the entire model is hydrated and sent on every Livewire request. Use <code>#[Locked]</code> for properties the client must never modify, and prefer IDs over full models when you can.
Where Livewire 4 fits, and where it does not
Livewire is now the right default for admin panels, settings pages, dashboards, multi-step forms, and most CRUD. It is the wrong tool for collaborative editors, real-time canvases, or anything where every keystroke needs sub-50ms feedback. For those, lean on Alpine for the interaction layer and Livewire to persist the result.
Success
Ship it
A Livewire 4 dashboard, deployed on a $20 Forge box behind Cloudflare, will comfortably serve thousands of authenticated users. Reach for SPAs only when product requirements actually need them.