Advanced Livewire Patterns
Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple. In this lesson, we'll explore advanced Livewire patterns including nested components, real-time features, file uploads, and integration with Alpine.js for enhanced interactivity.
Nested Components Communication
Livewire components can be nested and communicate with each other through various methods:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Product;
class ProductList extends Component
{
public $selectedProduct = null;
// Listen for events from child components
protected $listeners = [
'productSelected' => 'handleProductSelected',
'productDeleted' => 'refreshList',
];
public function handleProductSelected($productId)
{
$this->selectedProduct = Product::find($productId);
// Emit event to other components
$this->emit('productDetailsLoaded', $this->selectedProduct->id);
}
public function refreshList()
{
$this->selectedProduct = null;
// Refresh specific child component
$this->emit('refreshComponent', 'product-card');
}
public function render()
{
return view('livewire.product-list', [
'products' => Product::with('category')->latest()->get(),
]);
}
}
// Child component
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Product;
class ProductCard extends Component
{
public Product $product;
public bool $showDetails = false;
protected $listeners = ['refreshComponent'];
public function selectProduct()
{
// Emit to parent component
$this->emitUp('productSelected', $this->product->id);
}
public function deleteProduct()
{
$this->product->delete();
// Emit to all components
$this->emitSelf('productDeleted');
// Show success message
$this->dispatchBrowserEvent('product-deleted', [
'message' => 'Product deleted successfully!',
]);
}
public function refreshComponent()
{
// Refresh this component's data
$this->product->refresh();
}
public function render()
{
return view('livewire.product-card');
}
}
Note: Use emit() to broadcast to all components, emitUp() to send to parent, emitSelf() to send to the same component, and emitTo() to send to specific component by name.
Wire:model Deep Binding
Advanced data binding techniques for complex nested data structures:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class OrderForm extends Component
{
public $order = [
'customer' => [
'name' => '',
'email' => '',
'address' => [
'street' => '',
'city' => '',
'country' => '',
],
],
'items' => [],
'shipping_method' => 'standard',
'notes' => '',
];
public $products = [];
protected $rules = [
'order.customer.name' => 'required|min:3',
'order.customer.email' => 'required|email',
'order.customer.address.street' => 'required',
'order.customer.address.city' => 'required',
'order.customer.address.country' => 'required',
'order.items.*.product_id' => 'required|exists:products,id',
'order.items.*.quantity' => 'required|integer|min:1',
];
public function mount()
{
$this->products = Product::all();
}
public function addItem()
{
$this->order['items'][] = [
'product_id' => '',
'quantity' => 1,
'price' => 0,
];
}
public function removeItem($index)
{
unset($this->order['items'][$index]);
$this->order['items'] = array_values($this->order['items']);
}
// Real-time validation for specific field
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
// Calculate total in real-time
public function getCalculatedTotalProperty()
{
return collect($this->order['items'])->sum(function ($item) {
return ($item['quantity'] ?? 0) * ($item['price'] ?? 0);
});
}
public function submitOrder()
{
$this->validate();
$order = Order::create([
'customer_data' => $this->order['customer'],
'shipping_method' => $this->order['shipping_method'],
'notes' => $this->order['notes'],
'total' => $this->calculatedTotal,
]);
foreach ($this->order['items'] as $item) {
$order->items()->create($item);
}
session()->flash('success', 'Order created successfully!');
return redirect()->route('orders.show', $order);
}
public function render()
{
return view('livewire.order-form');
}
}
Blade view with deep wire:model binding:
<!-- resources/views/livewire/order-form.blade.php -->
<div>
<form wire:submit.prevent="submitOrder">
<!-- Customer Information -->
<div class="section">
<h3>Customer Information</h3>
<input type="text"
wire:model.defer="order.customer.name"
placeholder="Full Name">
@error('order.customer.name') <span class="error">{{ $message }}</span> @enderror
<input type="email"
wire:model.lazy="order.customer.email"
placeholder="Email">
@error('order.customer.email') <span class="error">{{ $message }}</span> @enderror
<!-- Address fields with deep binding -->
<input type="text"
wire:model="order.customer.address.street"
placeholder="Street Address">
<input type="text"
wire:model="order.customer.address.city"
placeholder="City">
<select wire:model="order.customer.address.country">
<option value="">Select Country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
</div>
<!-- Order Items -->
<div class="section">
<h3>Order Items</h3>
@foreach($order['items'] as $index => $item)
<div class="item-row" wire:key="item-{{ $index }}">
<select wire:model="order.items.{{ $index }}.product_id">
<option value="">Select Product</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name }}</option>
@endforeach
</select>
<input type="number"
wire:model="order.items.{{ $index }}.quantity"
min="1">
<button type="button"
wire:click="removeItem({{ $index }})">
Remove
</button>
</div>
@endforeach
<button type="button" wire:click="addItem">Add Item</button>
</div>
<!-- Total -->
<div class="total">
Total: ${{ number_format($this->calculatedTotal, 2) }}
</div>
<button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Submit Order</span>
<span wire:loading>Processing...</span>
</button>
</form>
</div>
Tip: Use wire:model.defer to batch updates and reduce server requests, wire:model.lazy to update on blur, and wire:model (or wire:model.live in v3) for real-time updates.
Deferred Loading with Placeholders
Improve perceived performance by loading heavy components after initial page render:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Report;
class AnalyticsDashboard extends Component
{
public $readyToLoad = false;
public $selectedPeriod = '30days';
// Load data only when component becomes visible
public function loadData()
{
$this->readyToLoad = true;
}
public function render()
{
return view('livewire.analytics-dashboard', [
'analytics' => $this->readyToLoad
? $this->getAnalytics()
: null,
]);
}
private function getAnalytics()
{
// Expensive query
sleep(2); // Simulate heavy computation
return [
'revenue' => Report::revenue($this->selectedPeriod),
'orders' => Report::orders($this->selectedPeriod),
'customers' => Report::customers($this->selectedPeriod),
'chart_data' => Report::chartData($this->selectedPeriod),
];
}
}
Blade view with loading placeholder:
<!-- resources/views/livewire/analytics-dashboard.blade.php -->
<div>
<h2>Analytics Dashboard</h2>
<select wire:model="selectedPeriod" wire:change="loadData">
<option value="7days">Last 7 Days</option>
<option value="30days">Last 30 Days</option>
<option value="90days">Last 90 Days</option>
</select>
@if($readyToLoad)
@if($analytics)
<div class="stats-grid">
<div class="stat-card">
<h3>Revenue</h3>
<p>${{ number_format($analytics['revenue'], 2) }}</p>
</div>
<div class="stat-card">
<h3>Orders</h3>
<p>{{ $analytics['orders'] }}</p>
</div>
<div class="stat-card">
<h3>Customers</h3>
<p>{{ $analytics['customers'] }}</p>
</div>
</div>
@else
<div wire:loading>
<div class="spinner"></div>
<p>Loading analytics...</p>
</div>
@endif
@else
<div class="placeholder">
<button wire:click="loadData">Load Analytics</button>
</div>
@endif
</div>
Polling for Real-time Updates
Automatically refresh component data at intervals:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Order;
class OrderMonitor extends Component
{
public $pollingInterval = 5000; // milliseconds
public $isPolling = true;
public function togglePolling()
{
$this->isPolling = !$this->isPolling;
}
public function render()
{
return view('livewire.order-monitor', [
'pendingOrders' => Order::pending()->latest()->take(10)->get(),
'activeOrders' => Order::active()->count(),
]);
}
}
<!-- Blade view with polling -->
<div wire:poll.{{ $isPolling ? $pollingInterval : 'keep-alive' }}ms>
<div class="header">
<h2>Order Monitor</h2>
<button wire:click="togglePolling">
{{ $isPolling ? 'Pause' : 'Resume' }} Monitoring
</button>
<span class="badge">{{ $activeOrders }} Active</span>
</div>
<div class="orders-list">
@foreach($pendingOrders as $order)
<div class="order-card" wire:key="order-{{ $order->id }}">
<h4>Order #{{ $order->id }}</h4>
<p>{{ $order->customer_name }}</p>
<span class="status">{{ $order->status }}</span>
<time>{{ $order->created_at->diffForHumans() }}</time>
</div>
@endforeach
</div>
<div wire:loading.delay>
<span class="updating-indicator">Updating...</span>
</div>
</div>
<!-- Poll specific action instead of entire component -->
<div>
<button wire:click="checkNewOrders" wire:poll.5s="checkNewOrders">
Check for New Orders
</button>
</div>
Warning: Polling can increase server load significantly. Use it sparingly and consider WebSockets (Laravel Echo + Pusher/Soketi) for high-frequency real-time updates.
File Uploads with Progress
Handle file uploads with real-time progress tracking and validation:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Storage;
class DocumentUploader extends Component
{
use WithFileUploads;
public $document;
public $documents = [];
public $uploadProgress = 0;
protected $rules = [
'document' => 'required|file|max:10240|mimes:pdf,doc,docx', // 10MB max
];
// Real-time validation during upload
public function updatedDocument()
{
$this->validate([
'document' => 'required|file|max:10240|mimes:pdf,doc,docx',
]);
}
public function save()
{
$this->validate();
$filename = $this->document->getClientOriginalName();
$path = $this->document->storeAs(
'documents',
$filename,
'public'
);
// Save to database
auth()->user()->documents()->create([
'filename' => $filename,
'path' => $path,
'size' => $this->document->getSize(),
'mime_type' => $this->document->getMimeType(),
]);
// Clear the file input
$this->document = null;
$this->uploadProgress = 0;
session()->flash('message', 'Document uploaded successfully!');
}
public function deleteDocument($documentId)
{
$document = auth()->user()->documents()->findOrFail($documentId);
Storage::disk('public')->delete($document->path);
$document->delete();
$this->emit('documentDeleted');
}
public function render()
{
return view('livewire.document-uploader', [
'uploadedDocuments' => auth()->user()->documents()->latest()->get(),
]);
}
}
<!-- Blade view with upload progress -->
<div>
<form wire:submit.prevent="save">
<div class="upload-area">
<input type="file"
wire:model="document"
id="document"
accept=".pdf,.doc,.docx">
<label for="document">
<span wire:loading.remove wire:target="document">
Choose a document
</span>
<span wire:loading wire:target="document">
Uploading...
</span>
</label>
@error('document')
<span class="error">{{ $message }}</span>
@enderror
</div>
<!-- Upload Progress -->
<div wire:loading wire:target="document" class="progress-bar">
<div class="progress-fill"
x-data="{ progress: 0 }"
x-on:livewire-upload-progress="progress = $event.detail.progress"
:style="`width: ${progress}%`">
</div>
<span x-text="`${progress}%`"></span>
</div>
@if($document)
<div class="file-preview">
<p>Selected: {{ $document->getClientOriginalName() }}</p>
<p>Size: {{ round($document->getSize() / 1024, 2) }} KB</p>
</div>
<button type="submit"
wire:loading.attr="disabled"
wire:target="document,save">
Upload Document
</button>
@endif
</form>
<!-- Uploaded Documents List -->
<div class="documents-list">
@foreach($uploadedDocuments as $doc)
<div class="document-item" wire:key="doc-{{ $doc->id }}">
<span>{{ $doc->filename }}</span>
<button wire:click="deleteDocument({{ $doc->id }})"
wire:confirm="Are you sure you want to delete this document?">
Delete
</button>
</div>
@endforeach
</div>
</div>
Alpine.js Integration
Combine Livewire's backend reactivity with Alpine.js for smooth frontend interactions:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class SearchableDropdown extends Component
{
public $search = '';
public $selected = null;
public function selectItem($itemId)
{
$this->selected = $itemId;
$this->emit('itemSelected', $itemId);
}
public function render()
{
$items = $this->search
? Item::where('name', 'like', "%{$this->search}%")->take(10)->get()
: [];
return view('livewire.searchable-dropdown', ['items' => $items]);
}
}
<!-- Alpine.js + Livewire combined -->
<div x-data="{ open: false }" @click.away="open = false">
<div class="dropdown">
<input type="text"
wire:model.debounce.300ms="search"
@focus="open = true"
placeholder="Search items...">
<div x-show="open"
x-transition
class="dropdown-menu">
@if($items->isNotEmpty())
@foreach($items as $item)
<div class="dropdown-item"
wire:click="selectItem({{ $item->id }})"
@click="open = false">
{{ $item->name }}
</div>
@endforeach
@else
<div class="no-results">
No items found
</div>
@endif
</div>
</div>
</div>
<!-- Modal with Alpine.js animations -->
<div x-data="{ showModal: @entangle('showModal') }">
<button @click="showModal = true">Open Modal</button>
<div x-show="showModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-90"
class="modal-overlay">
<div class="modal-content">
<h2>Modal Title</h2>
<p>Modal content here</p>
<button wire:click="save" @click="showModal = false">
Save & Close
</button>
</div>
</div>
</div>
Tip: Use @entangle() to create two-way data binding between Livewire and Alpine.js. This allows Alpine to read and update Livewire properties without making server requests.
Exercise 1: Build a todo list component where parent component manages the todo list and child components render individual todo items. Implement: add todo (parent), toggle complete (child emits to parent), delete todo (child emits to parent), and real-time search filtering.
Exercise 2: Create a multi-step form with deferred loading for each step. Step 1: Personal info, Step 2: Address (loads when clicked), Step 3: Payment (loads when clicked). Add form progress bar and validation that prevents moving forward if current step has errors.
Exercise 3: Build an image gallery uploader that allows multiple file uploads with individual progress bars. Show thumbnails after upload, allow drag-and-drop reordering (Alpine.js), and bulk delete. Add image preview modal with Alpine.js animations when clicking a thumbnail.
Summary
In this lesson, you learned:
- Nested component communication with events and listeners
- Deep wire:model binding for complex data structures
- Deferred loading patterns for improved performance
- Real-time polling for live updates
- File uploads with progress tracking
- Alpine.js integration for enhanced frontend interactivity