Laravel Livewire: Full-Stack Without the SPA Nightmare
Forget React. Forget Vue. Laravel Livewire delivers dynamic user interfaces using server-side PHP and minimal JavaScript. Build full-stack applications without the complexity of modern SPAs.
The Problem with Traditional SPAs
Modern web development has become a nightmare:
- Complex JavaScript build systems
- API/frontend synchronization hell
- State management complexity
- SEO and performance issues
- Massive bundle sizes
Livewire eliminates this complexity entirely.
What is Laravel Livewire?
Livewire is a full-stack framework for Laravel that makes building dynamic interfaces as simple as writing vanilla PHP. No APIs, no JavaScript frameworks, no build tools.
Core Principles
- Server-side rendering: Components rendered on the server
- AJAX magic: Automatic DOM diffing and updating
- PHP-first: Write logic in PHP, not JavaScript
- Progressive enhancement: Works with or without JavaScript
Building a Real-Time Todo Application
1. Create the Livewire Component
php artisan make:livewire TodoList
<?php
namespace App\Http\Livewire;
use App\Models\Todo;
use Livewire\Component;
class TodoList extends Component
{
public $todos;
public $newTodo = '';
public $editingId = null;
public $editingText = '';
protected $listeners = ['todoAdded' => 'refreshTodos'];
public function mount()
{
$this->refreshTodos();
}
public function refreshTodos()
{
$this->todos = Todo::orderBy('created_at', 'desc')->get();
}
public function addTodo()
{
$this->validate([
'newTodo' => 'required|min:3|max:255'
]);
Todo::create([
'text' => $this->newTodo,
'completed' => false
]);
$this->newTodo = '';
$this->refreshTodos();
// Emit event for other components
$this->emit('todoAdded');
session()->flash('message', 'Todo added successfully!');
}
public function toggleComplete($todoId)
{
$todo = Todo::find($todoId);
if ($todo) {
$todo->update(['completed' => !$todo->completed]);
$this->refreshTodos();
}
}
public function startEditing($todoId, $text)
{
$this->editingId = $todoId;
$this->editingText = $text;
}
public function saveEdit()
{
$this->validate([
'editingText' => 'required|min:3|max:255'
]);
$todo = Todo::find($this->editingId);
if ($todo) {
$todo->update(['text' => $this->editingText]);
}
$this->cancelEdit();
$this->refreshTodos();
}
public function cancelEdit()
{
$this->editingId = null;
$this->editingText = '';
}
public function deleteTodo($todoId)
{
Todo::find($todoId)?->delete();
$this->refreshTodos();
}
public function render()
{
return view('livewire.todo-list');
}
}
2. Create the Blade Template
resources/views/livewire/todo-list.blade.php
<div class="max-w-2xl mx-auto p-6">
<div class="bg-white rounded-lg shadow-lg p-6">
<h1 class="text-3xl font-bold text-gray-800 mb-6">Todo List</h1>
@if (session()->has('message'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
</div>
@endif
<!-- Add Todo Form -->
<form wire:submit.prevent="addTodo" class="mb-6">
<div class="flex gap-2">
<input
type="text"
wire:model.defer="newTodo"
placeholder="Add a new todo..."
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<button
type="submit"
class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
wire:loading.attr="disabled"
>
<span wire:loading.remove>Add</span>
<span wire:loading>Adding...</span>
</button>
</div>
@error('newTodo')
<span class="text-red-500 text-sm mt-1 block"></span>
@enderror
</form>
<!-- Todo List -->
<div class="space-y-2">
@forelse($todos as $todo)
<div class="flex items-center gap-3 p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<!-- Checkbox -->
<input
type="checkbox"
wire:click="toggleComplete()"
class="w-5 h-5 text-blue-600 rounded"
>
<!-- Todo Text -->
<div class="flex-1">
@if($editingId === $todo->id)
<div class="flex gap-2">
<input
type="text"
wire:model.defer="editingText"
wire:keydown.enter="saveEdit"
wire:keydown.escape="cancelEdit"
class="flex-1 px-2 py-1 border border-gray-300 rounded"
autofocus
>
<button
wire:click="saveEdit"
class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600"
>
Save
</button>
<button
wire:click="cancelEdit"
class="px-3 py-1 bg-gray-500 text-white rounded text-sm hover:bg-gray-600"
>
Cancel
</button>
</div>
@else
<span
class=""
wire:click="startEditing(, '')"
>
</span>
@endif
</div>
<!-- Actions -->
@if($editingId !== $todo->id)
<button
wire:click="deleteTodo()"
class="text-red-500 hover:text-red-700 transition-colors"
onclick="return confirm('Are you sure?')"
>
Delete
</button>
@endif
</div>
@error('editingText')
<span class="text-red-500 text-sm mt-1 block"></span>
@enderror
@empty
<div class="text-center py-8 text-gray-500">
No todos yet. Add one above!
</div>
@endforelse
</div>
</div>
</div>
3. Todo Model and Migration
php artisan make:model Todo -m
Migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->string('text');
$table->boolean('completed')->default(false);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('todos');
}
};
Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Todo extends Model
{
protected $fillable = ['text', 'completed'];
protected $casts = [
'completed' => 'boolean'
];
}
Advanced Livewire Features
Real-Time Updates with Broadcasting
class TodoList extends Component
{
protected $listeners = ['echo:todos,TodoUpdated' => 'refreshTodos'];
public function addTodo()
{
// ... validation and creation
// Broadcast to all connected clients
broadcast(new TodoUpdated($todo));
$this->refreshTodos();
}
}
File Uploads
use Livewire\WithFileUploads;
class FileUploader extends Component
{
use WithFileUploads;
public $photo;
public function save()
{
$this->validate([
'photo' => 'image|max:1024', // 1MB Max
]);
$path = $this->photo->store('photos');
// Save to database
auth()->user()->update(['avatar' => $path]);
session()->flash('message', 'Photo uploaded successfully!');
}
public function render()
{
return view('livewire.file-uploader');
}
}
<div>
@if ($photo)
<img src="" class="w-32 h-32 object-cover rounded">
@endif
<input type="file" wire:model="photo">
@error('photo') <span class="error"></span> @enderror
<button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save Photo</span>
<span wire:loading>Uploading...</span>
</button>
</div>
Pagination
use Livewire\WithPagination;
class PostsList extends Component
{
use WithPagination;
public $search = '';
public function updatingSearch()
{
$this->resetPage();
}
public function render()
{
return view('livewire.posts-list', [
'posts' => Post::where('title', 'like', '%' . $this->search . '%')
->paginate(10)
]);
}
}
Performance Optimization
1. Defer Loading Heavy Components
class ExpensiveComponent extends Component
{
public $readyToLoad = false;
public function loadData()
{
$this->readyToLoad = true;
}
public function render()
{
return view('livewire.expensive-component', [
'data' => $this->readyToLoad ? $this->getExpensiveData() : []
]);
}
}
2. Optimize Network Requests
class OptimizedComponent extends Component
{
// Only update when explicitly called
public function hydrate()
{
// Runs on every request
}
// Lazy load properties
public function getDataProperty()
{
return cache()->remember('expensive-data', 3600, function () {
return $this->calculateExpensiveData();
});
}
}
When to Use Livewire
✅ Perfect For:
- Admin panels and dashboards
- Forms with complex validation
- Real-time interfaces
- CRUD applications
- Progressive enhancement
❌ Consider Alternatives When:
- Building API-first applications
- Need offline functionality
- Heavy client-side interactions
- Mobile app development
- Performance-critical SPAs
Livewire vs. Traditional SPAs
| Feature | Livewire | React/Vue |
|---|---|---|
| Learning Curve | Low | High |
| Bundle Size | ~58KB | 100KB+ |
| SEO | Excellent | Complex |
| Development Speed | Fast | Slow |
| Backend Integration | Seamless | API Required |
Security Considerations
Input Validation
public function updatePost()
{
$this->validate([
'title' => 'required|max:255',
'content' => 'required|min:10'
]);
// Authorization
$this->authorize('update', $this->post);
$this->post->update($this->only(['title', 'content']));
}
Mass Assignment Protection
protected $fillable = ['title', 'content']; // In Livewire component
public function updatePost()
{
$this->post->update($this->only($this->fillable));
}
Conclusion
Laravel Livewire transforms full-stack development by eliminating the complexity of modern JavaScript frameworks while maintaining reactivity and user experience.
Key Benefits:
- ⚡ Rapid Development: Build features faster
- 🎯 Simple Mental Model: Think in PHP, not JavaScript
- 🔒 Built-in Security: Laravel’s security by default
- 📱 SEO Friendly: Server-side rendering
- 🛠️ Laravel Integration: Seamless ecosystem integration
Livewire proves you don’t need complex SPAs to build modern, reactive applications. Sometimes the best solution is the simplest one.
Ready to ditch JavaScript frameworks? Laravel Livewire makes full-stack development enjoyable again.