DEV Community

Marcc Atayde
Marcc Atayde

Posted on

Real-Time Laravel: Building Live Dashboards, Notifications, and Chat with Livewire

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'));
    }
}
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode
<!-- resources/views/livewire/order-counter.blade.php -->
<div class="text-4xl font-bold text-center">
    {{ $count }} Orders Today
</div>
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)