Master Livewire from the ground up. Learn why Livewire was created, understand core concepts like components, data binding, events, lifecycle hooks, and validation. Build a complete production-ready task management app with Livewire covering all fundamentals with best practices.

Livewire revolutionizes Laravel development by enabling developers to build interactive, reactive user interfaces without writing JavaScript. Instead of manually managing AJAX requests and DOM updates, Livewire handles the complexity behind the scenes. But why does Livewire exist, and what makes it fundamentally different?
In this article, we'll explore Livewire's philosophy, understand why it was created, dive deep into core concepts, and build a complete production-ready task management application that demonstrates all fundamental Livewire patterns.
Before Livewire, Laravel developers faced significant challenges:
Caleb Porzio created Livewire in 2019 with a revolutionary approach:
Livewire components are PHP classes that manage state and handle user interactions.
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function decrement(): void
{
$this->count--;
}
public function render()
{
return view('livewire.counter');
}
}Two-way data binding automatically syncs data between component and view.
<div>
<h1>Counter: {{ $count }}</h1>
<!-- wire:model creates two-way binding -->
<input type="text" wire:model="name" placeholder="Enter name" />
<p>Hello, {{ $name }}!</p>
<!-- Debounce updates for performance -->
<input type="text" wire:model.debounce-500ms="email" />
<!-- Lazy updates on blur -->
<input type="text" wire:model.lazy="phone" />
<!-- Throttle updates -->
<input type="text" wire:model.throttle-1000ms="search" />
</div>Handle user interactions by calling component methods.
<?php
namespace App\Livewire;
use Livewire\Component;
class TodoList extends Component
{
public array $todos = [];
public string $newTodo = '';
public function addTodo(): void
{
if (empty($this->newTodo)) {
return;
}
$this->todos[] = [
'id' => uniqid(),
'title' => $this->newTodo,
'completed' => false,
];
$this->newTodo = '';
}
public function toggleTodo(string $id): void
{
foreach ($this->todos as &$todo) {
if ($todo['id'] === $id) {
$todo['completed'] = !$todo['completed'];
break;
}
}
}
public function deleteTodo(string $id): void
{
$this->todos = array_filter(
$this->todos,
fn($todo) => $todo['id'] !== $id
);
}
public function render()
{
return view('livewire.todo-list');
}
}Livewire provides hooks to run code at specific times in a component's life.
<?php
namespace App\Livewire;
use Livewire\Component;
class UserProfile extends Component
{
public string $userId;
public array $user = [];
// Runs when component is instantiated
public function mount(string $userId): void
{
$this->userId = $userId;
$this->loadUser();
}
// Runs before rendering
public function hydrate(): void
{
// Refresh data if needed
}
// Runs after rendering
public function rendered(): void
{
// Dispatch events, log, etc.
}
// Runs before updating a property
public function updating(string $name, mixed $value): void
{
// Validate or transform value
}
// Runs after updating a property
public function updated(string $name, mixed $value): void
{
if ($name === 'email') {
$this->validateEmail($value);
}
}
private function loadUser(): void
{
$this->user = [
'id' => $this->userId,
'name' => 'John Doe',
'email' => 'john@example.com',
];
}
public function render()
{
return view('livewire.user-profile');
}
}Validate user input with Laravel's validation rules.
<?php
namespace App\Livewire;
use Livewire\Component;
class ContactForm extends Component
{
public string $name = '';
public string $email = '';
public string $message = '';
// Define validation rules
protected array $rules = [
'name' => 'required|min:3|max:50',
'email' => 'required|email',
'message' => 'required|min:10|max:1000',
];
// Custom error messages
protected array $messages = [
'name.required' => 'Please enter your name',
'email.email' => 'Please enter a valid email',
];
public function submit(): void
{
// Validate all properties
$this->validate();
// Process form
// ...
$this->reset();
session()->flash('success', 'Message sent successfully!');
}
// Validate single property
public function updatedEmail(): void
{
$this->validateOnly('email');
}
public function render()
{
return view('livewire.contact-form');
}
}Cache expensive calculations and recompute when dependencies change.
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Computed;
class ShoppingCart extends Component
{
public array $items = [];
// Computed property - cached until dependencies change
#[Computed]
public function total()
{
return collect($this->items)
->sum(fn($item) => $item['price'] * $item['quantity']);
}
#[Computed]
public function itemCount()
{
return collect($this->items)
->sum(fn($item) => $item['quantity']);
}
public function addItem(array $item): void
{
$this->items[] = $item;
// total() and itemCount() will be recomputed
}
public function render()
{
return view('livewire.shopping-cart');
}
}Communicate between components using events.
<?php
namespace App\Livewire;
use Livewire\Component;
class NotificationButton extends Component
{
public function notify(): void
{
// Dispatch event to other components
$this->dispatch('notification-sent', message: 'Hello!');
}
public function render()
{
return view('livewire.notification-button');
}
}
// Listening component
class NotificationCenter extends Component
{
public array $notifications = [];
// Listen for events
protected $listeners = [
'notification-sent' => 'addNotification',
];
public function addNotification(string $message): void
{
$this->notifications[] = [
'id' => uniqid(),
'message' => $message,
'timestamp' => now(),
];
}
public function render()
{
return view('livewire.notification-center');
}
}Show/hide elements based on component state.
<div>
<!-- Simple if -->
@if ($isLoggedIn)
<p>Welcome, {{ $userName }}!</p>
@else
<p>Please log in</p>
@endif
<!-- If/else if/else -->
@if ($status === 'pending')
<span class="badge badge-warning">Pending</span>
@elseif ($status === 'completed')
<span class="badge badge-success">Completed</span>
@else
<span class="badge badge-danger">Failed</span>
@endif
<!-- Unless (opposite of if) -->
@unless ($isAdmin)
<p>You don't have admin access</p>
@endunless
<!-- Ternary-like with isset -->
@isset($user)
<p>User: {{ $user->name }}</p>
@endisset
</div>Render lists efficiently with proper keys.
<div>
<!-- Basic loop -->
@foreach ($items as $item)
<div>{{ $item['name'] }}</div>
@endforeach
<!-- Loop with index -->
@foreach ($items as $index => $item)
<div>{{ $index + 1 }}. {{ $item['name'] }}</div>
@endforeach
<!-- Loop with $loop variable -->
@foreach ($items as $item)
<div>
@if ($loop->first)
<strong>First item</strong>
@endif
{{ $item['name'] }}
@if ($loop->last)
<strong>Last item</strong>
@endif
</div>
@endforeach
<!-- Empty state -->
@forelse ($items as $item)
<div>{{ $item['name'] }}</div>
@empty
<p>No items found</p>
@endforelse
</div>Style components with scoped CSS.
<div class="card">
<h2>{{ $title }}</h2>
<p>{{ $description }}</p>
</div>
<style scoped>
.card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
transition: all 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}
h2 {
margin: 0 0 10px 0;
font-size: 1.25rem;
}
p {
margin: 0;
color: #666;
}
</style>Let's build a complete task management application with Livewire.
laravel-app/
├── app/
│ ├── Livewire/
│ │ ├── TaskManager.php
│ │ ├── TaskForm.php
│ │ ├── TaskList.php
│ │ ├── TaskFilter.php
│ │ └── TaskStats.php
│ ├── Models/
│ │ └── Task.php
│ └── Http/
│ └── Controllers/
│ └── TaskController.php
├── resources/
│ └── views/
│ ├── livewire/
│ │ ├── task-manager.blade.php
│ │ ├── task-form.blade.php
│ │ ├── task-list.blade.php
│ │ ├── task-filter.blade.php
│ │ └── task-stats.blade.php
│ └── layouts/
│ └── app.blade.php
├── database/
│ └── migrations/
│ └── create_tasks_table.php
└── routes/
└── web.php<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Task extends Model
{
use HasFactory;
protected $fillable = [
'title',
'description',
'priority',
'due_date',
'completed',
];
protected $casts = [
'completed' => 'boolean',
'due_date' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function scopeActive($query)
{
return $query->where('completed', false);
}
public function scopeCompleted($query)
{
return $query->where('completed', true);
}
public function scopeByPriority($query, string $priority)
{
return $query->where('priority', $priority);
}
}<?php
namespace App\Livewire;
use App\Models\Task;
use Livewire\Component;
use Livewire\Attributes\Computed;
class TaskManager extends Component
{
public string $filter = 'all';
public string $sortBy = 'recent';
public string $searchQuery = '';
#[Computed]
public function tasks()
{
$query = Task::query();
// Apply filter
match ($this->filter) {
'active' => $query->active(),
'completed' => $query->completed(),
default => null,
};
// Apply search
if ($this->searchQuery) {
$query->where('title', 'like', "%{$this->searchQuery}%")
->orWhere('description', 'like', "%{$this->searchQuery}%");
}
// Apply sorting
match ($this->sortBy) {
'priority' => $query->orderByRaw("FIELD(priority, 'high', 'medium', 'low')"),
'due_date' => $query->orderBy('due_date'),
default => $query->latest(),
};
return $query->get();
}
#[Computed]
public function stats()
{
return [
'total' => Task::count(),
'active' => Task::active()->count(),
'completed' => Task::completed()->count(),
];
}
public function setFilter(string $filter): void
{
$this->filter = $filter;
}
public function setSortBy(string $sortBy): void
{
$this->sortBy = $sortBy;
}
public function render()
{
return view('livewire.task-manager');
}
}<?php
namespace App\Livewire;
use App\Models\Task;
use Livewire\Component;
class TaskForm extends Component
{
public string $title = '';
public string $description = '';
public string $priority = 'medium';
public string $dueDate = '';
protected array $rules = [
'title' => 'required|min:3|max:100',
'description' => 'nullable|max:500',
'priority' => 'required|in:low,medium,high',
'dueDate' => 'nullable|date|after:today',
];
public function submit(): void
{
$this->validate();
Task::create([
'title' => $this->title,
'description' => $this->description,
'priority' => $this->priority,
'due_date' => $this->dueDate ?: null,
'completed' => false,
]);
$this->reset();
$this->dispatch('task-created');
session()->flash('success', 'Task created successfully!');
}
public function render()
{
return view('livewire.task-form');
}
}<?php
namespace App\Livewire;
use App\Models\Task;
use Livewire\Component;
class TaskList extends Component
{
public array $tasks = [];
protected $listeners = [
'task-created' => 'refreshTasks',
];
public function mount(): void
{
$this->refreshTasks();
}
public function refreshTasks(): void
{
$this->tasks = Task::latest()->get()->toArray();
}
public function toggleTask(int $id): void
{
$task = Task::find($id);
if ($task) {
$task->update(['completed' => !$task->completed]);
$this->refreshTasks();
}
}
public function deleteTask(int $id): void
{
Task::find($id)?->delete();
$this->refreshTasks();
}
public function render()
{
return view('livewire.task-list', [
'tasks' => $this->tasks,
]);
}
}<?php
namespace App\Livewire;
use Livewire\Component;
class TaskFilter extends Component
{
public string $activeFilter = 'all';
public string $activeSortBy = 'recent';
public function setFilter(string $filter): void
{
$this->activeFilter = $filter;
$this->dispatch('filter-changed', filter: $filter);
}
public function setSortBy(string $sortBy): void
{
$this->activeSortBy = $sortBy;
$this->dispatch('sort-changed', sortBy: $sortBy);
}
public function render()
{
return view('livewire.task-filter');
}
}<?php
namespace App\Livewire;
use App\Models\Task;
use Livewire\Component;
use Livewire\Attributes\Computed;
class TaskStats extends Component
{
protected $listeners = [
'task-created' => '$refresh',
];
#[Computed]
public function stats()
{
return [
'total' => Task::count(),
'active' => Task::active()->count(),
'completed' => Task::completed()->count(),
'completionRate' => Task::count() > 0
? round((Task::completed()->count() / Task::count()) * 100)
: 0,
];
}
public function render()
{
return view('livewire.task-stats');
}
}<div class="task-manager">
<header class="task-header">
<h1>Task Manager</h1>
<p>Organize your tasks efficiently</p>
</header>
<main class="task-main">
<div class="container">
<!-- Task Stats -->
<livewire:task-stats />
<!-- Task Form -->
<livewire:task-form />
<!-- Task Filter -->
<livewire:task-filter />
<!-- Task List -->
<livewire:task-list />
</div>
</main>
</div>
<style scoped>
.task-manager {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.task-header {
text-align: center;
color: white;
padding: 40px 20px;
}
.task-header h1 {
margin: 0;
font-size: 2.5rem;
}
.task-header p {
margin: 10px 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
.task-main {
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
</style><form wire:submit="submit" class="task-form">
@if (session()->has('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
<div class="form-group">
<label for="title">Task Title *</label>
<input
type="text"
id="title"
wire:model="title"
placeholder="Enter task title"
class="form-input @error('title') is-invalid @enderror"
/>
@error('title')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
wire:model="description"
placeholder="Enter task description"
class="form-input"
rows="3"
></textarea>
@error('description')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div class="form-row">
<div class="form-group">
<label for="priority">Priority</label>
<select
id="priority"
wire:model="priority"
class="form-input"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label for="dueDate">Due Date</label>
<input
type="date"
id="dueDate"
wire:model="dueDate"
class="form-input"
/>
@error('dueDate')
<span class="error">{{ $message }}</span>
@enderror
</div>
</div>
<button type="submit" class="btn btn-primary">
Add Task
</button>
</form>
<style scoped>
.task-form {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #374151;
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-input.is-invalid {
border-color: #ef4444;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.error {
color: #ef4444;
font-size: 0.875rem;
margin-top: 5px;
display: block;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
width: 100%;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
</style><div class="task-list">
@forelse ($tasks as $task)
<div class="task-item" @class(['completed' => $task['completed']])>
<div class="task-content">
<input
type="checkbox"
@checked($task['completed'])
wire:click="toggleTask({{ $task['id'] }})"
class="task-checkbox"
/>
<div class="task-details">
<h3 class="task-title">{{ $task['title'] }}</h3>
@if ($task['description'])
<p class="task-description">{{ $task['description'] }}</p>
@endif
<div class="task-meta">
<span
class="priority-badge"
@class([
'priority-' . $task['priority'],
])
>
{{ ucfirst($task['priority']) }}
</span>
@if ($task['due_date'])
<span class="due-date">
Due: {{ $task['due_date']->format('M d, Y') }}
</span>
@endif
</div>
</div>
</div>
<button
wire:click="deleteTask({{ $task['id'] }})"
class="btn btn-danger btn-small"
>
Delete
</button>
</div>
@empty
<div class="empty-state">
<p>No tasks found. Create one to get started!</p>
</div>
@endforelse
</div>
<style scoped>
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f9fafb;
border-radius: 8px;
border-left: 4px solid #667eea;
transition: all 0.2s;
}
.task-item:hover {
background: #f3f4f6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.task-item.completed {
opacity: 0.6;
border-left-color: #10b981;
}
.task-item.completed .task-title {
text-decoration: line-through;
color: #9ca3af;
}
.task-content {
display: flex;
gap: 12px;
flex: 1;
}
.task-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
margin-top: 2px;
}
.task-details {
flex: 1;
}
.task-title {
font-size: 1rem;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
}
.task-description {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 8px;
}
.task-meta {
display: flex;
gap: 10px;
align-items: center;
}
.priority-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
.priority-high {
background: #ef4444;
}
.priority-medium {
background: #f59e0b;
}
.priority-low {
background: #3b82f6;
}
.due-date {
font-size: 0.875rem;
color: #6b7280;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-small {
padding: 6px 12px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
}
</style><div class="task-filter">
<div class="filter-buttons">
<button
wire:click="setFilter('all')"
@class(['filter-btn', 'active' => $activeFilter === 'all'])
>
All
</button>
<button
wire:click="setFilter('active')"
@class(['filter-btn', 'active' => $activeFilter === 'active'])
>
Active
</button>
<button
wire:click="setFilter('completed')"
@class(['filter-btn', 'active' => $activeFilter === 'completed'])
>
Completed
</button>
</div>
<div class="sort-buttons">
<select wire:change="setSortBy($event.target.value)" class="sort-select">
<option value="recent">Most Recent</option>
<option value="priority">By Priority</option>
<option value="due_date">By Due Date</option>
</select>
</div>
</div>
<style scoped>
.task-filter {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding: 15px;
background: #f9fafb;
border-radius: 8px;
flex-wrap: wrap;
gap: 15px;
}
.filter-buttons {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid #e5e7eb;
background: white;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.filter-btn.active {
border-color: #667eea;
background: #667eea;
color: white;
}
.sort-select {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
}
</style>// ✅ Keep components focused and single-responsibility
// ✅ Use computed properties for expensive calculations
// ✅ Leverage lifecycle hooks appropriately
// ✅ Use event dispatching for inter-component communication
// ❌ Avoid large monolithic components
// ❌ Don't perform heavy operations in render()
// ❌ Avoid tight coupling between components// ✅ Use wire:model for two-way binding
// ✅ Use debounce/throttle for performance
// ✅ Use lazy for blur events
// ✅ Validate on update when appropriate
// ❌ Avoid binding to large arrays
// ❌ Don't use wire:model on computed properties
// ❌ Avoid unnecessary re-renders// ✅ Define rules as class properties
// ✅ Use custom messages for better UX
// ✅ Validate on submit and on update
// ✅ Use validateOnly for specific fields
// ❌ Avoid validating everything on every keystroke
// ❌ Don't ignore validation errors
// ❌ Avoid complex validation logic in components// ✅ Use computed properties for caching
// ✅ Use pagination for large datasets
// ✅ Lazy load components with wire:lazy
// ✅ Use wire:loading to show loading states
// ❌ Avoid loading all data at once
// ❌ Don't perform N+1 queries
// ❌ Avoid unnecessary component re-renders// ✅ Always validate user input
// ✅ Use authorization checks
// ✅ Sanitize output in views
// ✅ Use CSRF protection (automatic with Livewire)
// ❌ Don't trust user input
// ❌ Avoid exposing sensitive data
// ❌ Don't skip authorization checks// ❌ Wrong - can't bind to computed properties
public function getTotal()
{
return $this->items->sum('price');
}
// ✅ Correct - use regular properties or computed attributes
#[Computed]
public function total()
{
return $this->items->sum('price');
}// ❌ Wrong - form data persists
public function submit()
{
$this->validate();
Task::create($this->toArray());
// Form data still visible
}
// ✅ Correct - reset after submission
public function submit()
{
$this->validate();
Task::create($this->toArray());
$this->reset();
}// ❌ Wrong - components don't communicate
class ComponentA extends Component {}
class ComponentB extends Component {}
// ✅ Correct - use event dispatching
class ComponentA extends Component
{
public function notify()
{
$this->dispatch('event-name', data: $value);
}
}
class ComponentB extends Component
{
protected $listeners = ['event-name' => 'handleEvent'];
public function handleEvent($data) {}
}// ❌ Wrong - N+1 queries
public function render()
{
return view('livewire.tasks', [
'tasks' => Task::all(), // Queries users for each task
]);
}
// ✅ Correct - eager load relationships
public function render()
{
return view('livewire.tasks', [
'tasks' => Task::with('user')->get(),
]);
}<!-- ❌ Wrong - no feedback during loading -->
<button wire:click="submit">Submit</button>
<!-- ✅ Correct - show loading state -->
<button wire:click="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Submit</span>
<span wire:loading>Loading...</span>
</button>Livewire revolutionizes Laravel development by enabling developers to build interactive, reactive user interfaces entirely in PHP. By eliminating the need for manual JavaScript and AJAX, Livewire dramatically improves developer experience and productivity.
The task management application we built demonstrates all core Livewire concepts in action. Understanding components, data binding, events, lifecycle hooks, and validation is essential for building modern Livewire applications.
Key takeaways:
Next steps:
Livewire is the future of Laravel development. Keep learning, building, and pushing the boundaries of what's possible with full-stack PHP.