← All Articles

Real-Time Infolist with `wire:poll` and Embedded Livewire Components

April 13, 2026

3 min read

Real-Time Infolist with wire:poll and Embedded Livewire Components

Your customer places an order. Your ops team watches it move through pending → processing → shipped → delivered — and they're sitting there hammering F5 every few seconds because the status panel hasn't moved. Sound familiar?

Filament's Infolist component is elegant for displaying structured read-only data. But there's a catch most developers hit at exactly the wrong moment: infolist entries are Blade views, not Livewire components. They render once on page load and stay frozen until the whole page refreshes. For a live order dashboard, a real-time sensor feed, or a payment status tracker — that's a deal-breaker.

The good news? The fix is clean, composable, and doesn't require abandoning Filament's infolist at all. Let's build it.


The Challenge

Consider a ViewOrder resource page. You have an Infolist with an OrderStatusEntry, a PaymentStatusEntry, and a few summary fields. The data behind these entries changes frequently — a webhook fires, a job runs, a human clicks a button in another panel — and you need the ops team to see those changes without refreshing the page.

Your first instinct might be to reach for wire:poll on the ViewRecord page itself. And that works — it re-renders the entire Livewire component on every tick. But that comes with two problems:

  1. The entire page flickers — every widget, every tab, every form re-renders. Users lose scroll position. It feels broken.
  2. You have no control over what gets polled — everything or nothing.

Your second instinct might be to use a TextEntry with a state closure and hope Filament does something clever. It won't. The closure runs at render time and never again.

What you actually need is surgical reactivity — only the status card re-renders on a tick, the rest of the page stays still.


The Strategy

The approach has three moving parts:

  1. A dedicated Livewire component that owns the real-time data and re-renders on a wire:poll interval. This is your reactive island.
  2. A custom Filament Entry class (CustomEntry) that renders your Livewire component's blade view inline inside the infolist schema — acting as a bridge between Filament's static rendering and Livewire's reactive model.
  3. The Livewire component receives the record's primary key as a public property, fetches fresh data on every poll tick, and displays it using its own template.

No full-page re-renders. No flicker. Precise control over polling intervals per entry. 🏊‍♂️


Implementation

Step 1: Create the Livewire Component

This component owns a single responsibility: fetch the freshest version of a record (or a subset of its data) and expose it to its template.

1php artisan make:livewire OrderStatusCard
1// file: app/Livewire/OrderStatusCard.php
2 
3<?php
4 
5namespace App\Livewire;
6 
7use App\Models\Order;
8use Livewire\Component;
9 
10class OrderStatusCard extends Component
11{
12 public int $orderId;
13 
14 public ?Order $order = null;
15 
16 public function mount(int $orderId): void
17 {
18 $this->orderId = $orderId;
19 $this->loadOrder();
20 }
21 
22 public function loadOrder(): void
23 {
24 $this->order = Order::query()
25 ->select(['id', 'status', 'payment_status', 'updated_at'])
26 ->findOrFail($this->orderId);
27 }
28 
29 public function render(): \Illuminate\View\View
30 {
31 return view('livewire.order-status-card');
32 }
33}

Notice loadOrder() is a separate public method — this will matter in the template where wire:poll calls it directly, rather than triggering a full component re-render.


Step 2: Create the Livewire Template

1{{-- file: resources/views/livewire/order-status-card.blade.php --}}
2 
3<div wire:poll.5s="loadOrder">
4 @if ($order)
5 <div class="flex flex-col gap-3 rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
6 
7 <div class="flex items-center justify-between">
8 <span class="text-sm font-medium text-gray-500 dark:text-gray-400">
9 Order Status
10 </span>
11 <span @class([
12 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
13 'bg-yellow-100 text-yellow-800' => $order->status === 'pending',
14 'bg-blue-100 text-blue-800' => $order->status === 'processing',
15 'bg-purple-100 text-purple-800' => $order->status === 'shipped',
16 'bg-green-100 text-green-800' => $order->status === 'delivered',
17 'bg-red-100 text-red-800' => $order->status === 'cancelled',
18 ])>
19 {{ str($order->status)->headline() }}
20 </span>
21 </div>
22 
23 <div class="flex items-center justify-between">
24 <span class="text-sm font-medium text-gray-500 dark:text-gray-400">
25 Payment
26 </span>
27 <span @class([
28 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
29 'bg-gray-100 text-gray-700' => $order->payment_status === 'unpaid',
30 'bg-green-100 text-green-800' => $order->payment_status === 'paid',
31 'bg-red-100 text-red-800' => $order->payment_status === 'refunded',
32 ])>
33 {{ str($order->payment_status)->headline() }}
34 </span>
35 </div>
36 
37 <p class="text-right text-xs text-gray-400 dark:text-gray-600">
38 Last updated: {{ $order->updated_at->diffForHumans() }}
39 </p>
40 
41 </div>
42 @endif
43</div>

wire:poll.5s="loadOrder" fires every 5 seconds and calls only loadOrder() — a targeted DB fetch, nothing else on the page moves. ✨


Step 3: Create a Custom Filament Entry

This is the bridge. A custom Entry that renders the Livewire component's HTML into the infolist schema using Blade::render().

1php artisan make:filament-infolist-entry OrderStatusEntry

If the generator doesn't exist for entries in your version, create the file manually:

1// file: app/Filament/Infolists/Components/OrderStatusEntry.php
2 
3<?php
4 
5namespace App\Filament\Infolists\Components;
6 
7use Filament\Infolists\Components\Entry;
8use Illuminate\Contracts\View\View;
9 
10class OrderStatusEntry extends Entry
11{
12 protected string $view = 'filament.infolists.components.order-status-entry';
13}
1{{-- file: resources/views/filament/infolists/components/order-status-entry.blade.php --}}
2 
3<x-dynamic-component
4 :component="$getEntryWrapperView()"
5 :entry="$entry"
6>
7 @livewire('order-status-card', ['orderId' => $getRecord()->getKey()], key('order-status-' . $getRecord()->getKey()))
8</x-dynamic-component>

The @livewire() directive mounts a fully independent Livewire component inside the Filament entry wrapper. The key() ensures Livewire doesn't re-use a stale component instance if you're on a list or repeating context.


Step 4: Register the Entry in the ViewRecord Infolist

1// file: app/Filament/Resources/OrderResource/Pages/ViewOrder.php
2 
3<?php
4 
5namespace App\Filament\Resources\OrderResource\Pages;
6 
7use App\Filament\Infolists\Components\OrderStatusEntry;
8use App\Filament\Resources\OrderResource;
9use Filament\Infolists\Components\Grid;
10use Filament\Infolists\Components\Section;
11use Filament\Infolists\Components\TextEntry;
12use Filament\Infolists\Infolist;
13use Filament\Resources\Pages\ViewRecord;
14 
15class ViewOrder extends ViewRecord
16{
17 protected static string $resource = OrderResource::class;
18 
19 public function infolist(Infolist $infolist): Infolist
20 {
21 return $infolist
22 ->schema([
23 Grid::make(3)
24 ->schema([
25 
26 Section::make('Order Details')
27 ->columnSpan(2)
28 ->schema([
29 TextEntry::make('id')
30 ->label('Order ID')
31 ->badge(),
32 
33 TextEntry::make('customer.name')
34 ->label('Customer'),
35 
36 TextEntry::make('total_amount')
37 ->label('Total')
38 ->money('usd'),
39 
40 TextEntry::make('created_at')
41 ->label('Placed At')
42 ->dateTime(),
43 ]),
44 
45 Section::make('Live Status')
46 ->columnSpan(1)
47 ->schema([
48 OrderStatusEntry::make('live_status')
49 ->label('')
50 ->columnSpanFull(),
51 ]),
52 
53 ]),
54 ]);
55 }
56}

The OrderDetails section is completely static — it renders once and stays. The Live Status section re-fetches every 5 seconds, independently. No flicker, no scroll-jump, no unnecessary queries for data that doesn't change. Pretty clean, right?


Step 5: Secure the Model with Selective Column Fetching

Your Order model likely has sensitive columns. Make the Livewire component explicit about what it selects:

1// file: app/Livewire/OrderStatusCard.php (loadOrder method update)
2 
3public function loadOrder(): void
4{
5 $this->order = Order::query()
6 ->select(['id', 'status', 'payment_status', 'updated_at'])
7 ->findOrFail($this->orderId);
8}

No accidental leakage of stripe_customer_id, internal_notes, or billing columns through the reactive component.


Generalising the Pattern

You're not limited to order status. The same pattern works for any live data island:

1// file: app/Livewire/SensorReadingCard.php
2 
3<?php
4 
5namespace App\Livewire;
6 
7use App\Models\Sensor;
8use Livewire\Component;
9 
10class SensorReadingCard extends Component
11{
12 public int $sensorId;
13 
14 public ?Sensor $sensor = null;
15 
16 public function mount(int $sensorId): void
17 {
18 $this->sensorId = $sensorId;
19 $this->loadSensor();
20 }
21 
22 public function loadSensor(): void
23 {
24 $this->sensor = Sensor::query()
25 ->select(['id', 'name', 'last_reading', 'unit', 'read_at'])
26 ->findOrFail($this->sensorId);
27 }
28 
29 public function render(): \Illuminate\View\View
30 {
31 return view('livewire.sensor-reading-card');
32 }
33}

Change the poll interval to .2s for high-frequency IoT data, or .30s for less volatile dashboards. The entry wrapper stays identical — only the Livewire component changes.


Pro Hint

Adaptive Polling: Pause When the Tab Is Hidden

wire:poll fires even when the browser tab is in the background. For a busy ops dashboard with 10 live cards each polling every 5 seconds, that's 10 DB queries every 5 seconds — for a user who isn't even looking.

Livewire 4 supports the .visible modifier out of the box:

1{{-- file: resources/views/livewire/order-status-card.blade.php --}}
2 
3<div wire:poll.5s.visible="loadOrder">
4 {{-- ... --}}
5</div>

wire:poll.5s.visible automatically pauses polling when the element (or its tab) is not visible in the viewport. The moment the user switches back, polling resumes. Zero extra JavaScript. Zero wasted queries.

For production dashboards with many concurrent users, this single modifier can cut your background query load by 60–80%. Pair it with a DB index on your status and updated_at columns and the SELECT is practically free:

1// file: database/migrations/xxxx_xx_xx_add_status_index_to_orders_table.php
2 
3Schema::table('orders', function (Blueprint $table): void {
4 $table->index(['status', 'updated_at']);
5});

Wrapping Up

Filament's infolist is a static renderer — and that's fine for 90% of use cases. But when your data is alive, you don't throw Filament away. You embed a Livewire island inside it, give that island its own polling heartbeat, and let the rest of the page stay perfectly still.

The pattern scales from a single status badge to a full real-time dashboard. Add as many CustomEntry wrappers as your UI needs, each with its own Livewire component and its own poll interval. Your ops team gets a live feed. Your database gets targeted queries. Your users stop hammering F5.

Over & out! 🚀

Newsletter

Get notified when new components and articles are published. No spam, just Filament goodness.

We will send you a confirmation link before you are subscribed.