Most web apps feel static until the moment they don't have to be. A user submits a form, and a counter updates somewhere else on the page — no refresh, no polling loop, no JavaScript framework sprawl. That's the promise of real-time UI, and in the Laravel ecosystem, Livewire makes it surprisingly achievable without abandoning the backend-first workflow you already know.
This article walks through three practical real-time patterns using Livewire: live notifications, a collaborative counter (think dashboards), and a basic chat interface. Each one builds on the last, and by the end you'll have a clear mental model for when to use Livewire alone versus when to bring in Laravel Echo and WebSockets.
What "Real-Time" Actually Means in Livewire
Livewire is not WebSockets by default. It uses AJAX under the hood — each component interaction triggers a server round-trip. For many use cases (form validation, live search, toggling UI state), this is fast enough to feel real-time.
True broadcast-driven real-time — where the server pushes data to the client without a user action — requires Laravel Echo paired with a WebSocket driver like Reverb (Laravel's first-party option as of 2024), Pusher, or Ably.
Knowing which tool fits which scenario saves you from over-engineering.
| Pattern | Livewire Only | Livewire + Echo |
|---|---|---|
| Live search | ✅ | ❌ overkill |
| Form validation | ✅ | ❌ overkill |
| Notifications pushed from server | ❌ polling hack | ✅ |
| Multi-user chat | ❌ | ✅ |
| Dashboard counters (user-triggered) | ✅ | depends |
Pattern 1: Live Search with Livewire
Start simple. A search input that filters results on every keystroke — no page reload.
// app/Livewire/ProductSearch.php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Product;
class ProductSearch extends Component
{
public string $query = '';
public function render()
{
$products = Product::when($this->query, function ($q) {
$q->where('name', 'like', '%' . $this->query . '%');
})->limit(10)->get();
return view('livewire.product-search', compact('products'));
}
}
<!-- resources/views/livewire/product-search.blade.php -->
<div>
<input
type="text"
wire:model.live.debounce.300ms="query"
placeholder="Search products..."
class="w-full border rounded px-4 py-2"
/>
<ul class="mt-4 space-y-2">
@foreach ($products as $product)
<li class="p-3 bg-white rounded shadow text-sm">{{ $product->name }}</li>
@endforeach
</ul>
</div>
The wire:model.live.debounce.300ms directive is doing real work here — it waits 300ms after the user stops typing before firing the request. Without debounce, every keystroke hits the server.
Pattern 2: Broadcasting Notifications with Reverb and Echo
Now for the real-time push scenario. A user completes an order, and an admin dashboard counter increments live — without the admin refreshing the page.
Step 1: Install and configure Laravel Reverb
php artisan install:broadcasting
This scaffolds Reverb, installs Echo, and sets up the BroadcastServiceProvider. Add your .env values:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080
Step 2: Create a broadcastable event
// app/Events/OrderPlaced.php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderPlaced implements ShouldBroadcast
{
public function __construct(public int $totalOrders) {}
public function broadcastOn(): Channel
{
return new Channel('dashboard');
}
public function broadcastAs(): string
{
return 'order.placed';
}
}
Step 3: Listen in a Livewire component
// app/Livewire/OrderCounter.php
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\On;
class OrderCounter extends Component
{
public int $count = 0;
public function getListeners()
{
return [
'echo:dashboard,order.placed' => 'refreshCount',
];
}
public function refreshCount($event)
{
$this->count = $event['totalOrders'];
}
public function render()
{
return view('livewire.order-counter');
}
}
<!-- resources/views/livewire/order-counter.blade.php -->
<div class="text-4xl font-bold text-center">
{{ $count }} Orders Today
</div>
When any part of your application fires event(new OrderPlaced($totalOrders)), the Livewire component reacts instantly — across every browser tab connected to that channel.
Pattern 3: A Simple Multi-User Chat
Chat is the classic WebSocket use case. Here's a minimal but functional implementation.
// app/Livewire/ChatRoom.php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Message;
use App\Events\MessageSent;
class ChatRoom extends Component
{
public string $newMessage = '';
public $messages;
public function mount()
{
$this->messages = Message::with('user')->latest()->take(50)->get()->reverse();
}
public function getListeners()
{
return [
'echo:chat,message.sent' => 'loadMessages',
];
}
public function loadMessages()
{
$this->messages = Message::with('user')->latest()->take(50)->get()->reverse();
}
public function sendMessage()
{
$this->validate(['newMessage' => 'required|max:500']);
$message = auth()->user()->messages()->create([
'body' => $this->newMessage,
]);
broadcast(new MessageSent($message))->toOthers();
$this->newMessage = '';
$this->loadMessages();
}
public function render()
{
return view('livewire.chat-room');
}
}
Note the .toOthers() call on broadcast — this prevents the message sender from receiving their own broadcast echo, since Livewire already updated their UI optimistically via loadMessages().
Performance Considerations
A few things to keep in mind as your real-time features scale:
Debounce aggressively. Every wire:model.live without debounce is a server hit. Use .debounce.300ms or .debounce.500ms for text inputs.
Use wire:loading for perceived performance. Even a 200ms server round-trip feels snappy if you show a loading indicator immediately.
<button wire:click="sendMessage" wire:loading.attr="disabled">
<span wire:loading.remove>Send</span>
<span wire:loading>Sending...</span>
</button>
Paginate broadcast listeners carefully. If you're broadcasting to thousands of users on a shared channel, consider presence channels or private channels with authorization to reduce noise.
Queue your broadcasts. Mark your events with ShouldBroadcastNow for immediate dispatch or let them queue for scale:
class MessageSent implements ShouldBroadcast
{
// Uses queue by default — fast for the HTTP request, async for the broadcast
}
When Livewire Isn't Enough
Livewire handles a lot, but there are scenarios where a dedicated JavaScript framework makes more sense: highly interactive canvas-based UIs, complex drag-and-drop editors, or apps where offline capability matters. That line gets blurry as Livewire matures, but it's worth acknowledging. If you're evaluating architecture for a complex product and want a second opinion from developers who work with these patterns daily, check it out — the team there has shipped several real-time Laravel applications in production and can help you map the right approach to your specific requirements.
Conclusion
Real-time features in Laravel don't require abandoning the backend-first workflow or wiring up a full JavaScript framework. Livewire covers the majority of interactive UI needs through its AJAX-based reactivity model, and when you need true server-push, Laravel Reverb and Echo integrate cleanly into the same component structure.
The key takeaway: start with wire:model.live for user-triggered interactivity, reach for broadcasting only when the server needs to initiate the update. Getting that distinction right keeps your architecture clean and your codebase maintainable.
Top comments (0)