Advanced Laravel

Advanced Livewire Patterns

18 min Lesson 37 of 40

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