calebporzio/livewire
Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving PHP. It bridges server and browser, enabling reactive components, form handling, and real-time updates with minimal JavaScript.
Livewire earns its place in a Laravel app as the interactive layer of Blade — not a bolt-on library. Two commands and you're running:
composer require livewire/livewire:^4.4 # pin the exact minor you validated
php artisan make:livewire UsersTable
That generates the two files you'll touch daily:
app/Livewire/UsersTable.php — a class extending Livewire\Component. Public properties are state, public methods are actions, render() returns the view.resources/views/livewire/users-table.blade.php — the template, wired up with wire: directives.Mount it anywhere in existing Blade with a tag — no route, no controller, no bundler entry:
<livewire:users-table />
Styles and scripts are injected automatically in v4; reach for @livewireStyles / @livewireScripts only when a custom layout needs explicit asset ordering.
Your day-to-day loop. Edit the component class, edit the Blade view, refresh the browser. The moment a component misbehaves, fall back to Livewire::test() — it isolates the server half from the wire in a fraction of a second. If the test passes but the browser doesn't, the problem is in the DOM/JS layer (window.Livewire, the network tab for the livewire/update request, Alpine conflicts).
Where to look when docs are ambiguous. The src/Features/Support*/ folders in vendor are the real spec — each has a Support* hook, a Handles* trait, a UnitTest.php, and a BrowserTest.php. Also check the docs for your major (/docs/4.x/); v2 snippets are everywhere online and are the most common source of "why doesn't this work."
First component worth shipping. A searchable, paginated list. It's the single most common thing you'll build, it replaces either a jQuery pile or an SPA page, and it exercises the four mechanics you'll reuse constantly: public state, computed data, wire:model modifier chains, and keyed loops.
class UsersTable extends Component
{
public string $search = '';
#[Computed]
public function users()
{
return User::query()
->where('name', 'like', "%{$this->search}%")
->paginate(15);
}
public function render()
{
return view('livewire.users-table');
}
}
<div>
<input type="search" wire:model.live.debounce.300ms="search">
@foreach ($this->users as $user)
<div wire:key="user-{{ $user->id }}">{{ $user->name }}</div>
@endforeach
{{ $this->users->links() }}
</div>
Ship that before reading anything else. Everything else in Livewire is a variation on it.
Public properties are the contract with the client — treat them like request input. Everything public gets serialized into the payload and comes back on every round-trip. Keep the surface to scalars, arrays, and IDs. When you need derived or expensive data, use #[Computed]: evaluated server-side per request, never serialized, memoized for the request duration only.
#[Computed]
public function board()
{
return Board::with('cards.author')->findOrFail($this->boardId);
}
Reference it as $this->board (no parentheses) in both classes and views.
Actions are public methods, and they must authorize themselves. There's no middleware guarding an individual wire:click. Do the scoped lookup inside the method — never Card::findOrFail($id) from a client-supplied ID:
public function deleteCard(int $cardId): void
{
$this->authorize('update', $this->board);
$this->board->cards()->findOrFail($cardId)->delete();
}
Forms: wire:model is deferred by default, and that's the right default. Validation on every keystroke is wasteful; validate on blur or on submit. Use updated() + validateOnly() for inline feedback without blocking typing:
#[Validate('required|email|unique:users,email')]
public string $email = '';
public function updated($property): void
{
$this->validateOnly($property);
}
public function save(): void
{
$data = $this->validate();
User::create($data);
$this->reset('email');
$this->dispatch('user-created');
}
Lifecycle hooks you'll actually use. mount() for route params and resolution, boot() for per-request setup that must run on both mount and updates, updated{Property}() for targeted side effects, render() for view data. hydrate()/dehydrate() are escape hatches for non-serializable collaborators — if you're reaching for them, ask whether the object can live in a #[Computed] or the container instead.
Cross-component communication via events, not shared parents. Child dispatches, sibling listens:
$this->dispatch('card-moved', cardId: $card->id, column: $column->id);
#[On('card-moved')]
public function refreshCard(int $cardId, int $column): void { /* ... */ }
This is the idiomatic replacement for v2's $emit/$listeners and it keeps components independently mountable.
Parent/child composition — one wire:key per loop iteration, always. The DOM diffing uses keys to establish identity across re-renders. On a reordered, filtered, or paginated list, missing keys manifest as wrong input values, duplicated rows, and Alpine state jumping between items — bugs that are painful to trace back to a missing attribute.
Polish without JavaScript, in order of how often you'll use it: wire:loading / wire:loading.remove / wire:target="save" for async feedback; wire:confirm for destructive actions; wire:dirty for unsaved-change hints; wire:poll.10s for dashboards; wire:ignore for third-party widget islands. These four cover most of what you'd otherwise write vanilla JS for.
Navigation. wire:navigate on internal links gives SPA-style transitions with persistent elements via @persist (audio players, sidebars, open dropdowns). Re-initialize third-party scripts on the livewire:navigated event — they won't get a fresh page load otherwise.
File uploads. use WithFileUploads; on the component, wire:model="photo" on the input, $this->photo->store('photos') in the action. Preview with temporaryUrl(). Set the temp disk explicitly if you're not on local storage — the default is local and it bites on multi-server setups.
Testing is where Livewire pays back the most. Nearly all behavior is testable without a browser, which makes component tests fast enough to keep in the inner loop:
Livewire::test(UsersTable::class)
->set('search', 'caleb')
->assertSee('Caleb')
->call('deleteCard', $card->id)
->assertDispatched('card-moved')
->assertHasErrors(['email' => 'required']);
Reach for a Dusk/Browser test only when the assertion genuinely lives in the DOM (Alpine interplay, wire:ignore islands, navigate transitions).
Strangler migration. A Livewire component is additive inside a Blade view — you can migrate one table or one form at a time and leave the surrounding controller-rendered page untouched. That turns "rewrite the admin section" into a sequence of independently reviewable, independently revertible PRs.
Extend, don't fork. For custom framework behavior, follow the package's own conventions: a Support{Feature} hook extending ComponentHook (boot/mount/hydrate/update/call/render/dehydrate), an optional Handles{Feature} trait mixed into Component, a thin attribute file, a wire-{name}.js directive imported in js/directives/index.js, and registration in LivewireServiceProvider::bootFeatures(). For most app-level needs a plain trait plus a Blade component is dramatically cheaper — reserve the full feature pattern for things you'd otherwise patch into vendor.
Public properties are user-mutable. They round-trip through the browser and can be tampered with. Never store passwords, API keys, or full Eloquent models in them. Mark identity props that must not change with #[Locked], and pass IDs rather than models across method boundaries.
wire:model being deferred is the #1 "my validation isn't firing" bug. Add .live. But be deliberate: .live on a search input without .debounce will hit the database on every keystroke. .blur is usually the better choice for form fields.
N+1 in render() and computed properties. render() runs on every update, and the #[Computed] cache is per-request — it will not save you across round-trips. Eager-load aggressively (->with(...)) and paginate anything list-shaped. A component that's fine at 50 rows will time out at 5,000.
wire:key in loops is not optional. Especially with filters, sorts, and pagination. If you're seeing phantom values in inputs, check keys first.
Third-party JS and re-renders fight each other. Select2, Chart.js, datepickers, anything that mutates the DOM will be clobbered on the next update. Wrap with wire:ignore (or wire:ignore.self when the wrapper itself needs to swap), initialize in x-init or behind a livewire:init listener, and re-run init on livewire:navigated.
Alpine version alignment. Livewire's JS assumes a specific Alpine version. If the app also loads its own Alpine, you get reactivity that's subtly broken and looks exactly like a Livewire bug. Either let Livewire bundle it, or pin both to the same version.
"Corrupt component payload" / checksum errors usually have three causes: the component's public property shape changed mid-flight (a deploy while a tab was open), APP_KEY rotated, or a wire:key was reused across structurally different components. Deploy-correlated 419s are the same family — long-lived wire:poll pages outlive the session. Plan for a "this page is stale, reload" UX rather than pretending it won't happen.
One root element per component view. Two siblings at the top level produce confusing update behavior that's very hard to trace back to the template.
Event bubbling on nested click targets. Use wire:click.stop on an icon inside a clickable row, or both handlers will fire.
Publish config only when you need it: php artisan livewire:publish --config. Defaults are right for most apps; temp-upload disk settings are the usual reason to touch it.
#[Renderless] for endpoints that shouldn't re-render — polling for status, analytics pings, prefetches. It skips the render pass and is a meaningful perceived-performance win on large components.
Upgrades are a project, not a patch. Majors have historically changed lifecycle behavior and attribute internals. Pin exact versions, keep an inventory of any Support* features you've written (each is internal API with a maintenance cost at every major bump), and read the upgrade guide end-to-end before touching composer.json.
Sanity-check package metadata before it influences a decision. The feed numbers on this repo have been demonstrably unreliable (single-digit stars for one of the most-used frameworks in the Laravel ecosystem, contradictory sub-scores). Verify version, license, and activity against Packagist and the actual repository. Don't let an unverified number act as either a veto or an endorsement — in either direction.
How can I help you explore Laravel packages today?