About us Guides Projects Contacts
Админка
please wait

Laravel Livewire enables building dynamic, reactive interfaces using PHP instead of JavaScript. It provides the reactivity of Vue or React while keeping your code in familiar Blade templates. This guide covers building modern, interactive UIs with Livewire from a senior developer's perspective.

Why Livewire

Livewire offers compelling advantages:

  1. PHP Only: Build reactive UIs without writing JavaScript.
  2. Blade Integration: Use familiar Laravel templating.
  3. Server-Side Logic: Keep business logic on the server.
  4. Real-Time Updates: Automatic DOM diffing and updates.
  5. Laravel Ecosystem: Works seamlessly with existing Laravel apps.

Installation

Install Livewire via Composer:

composer require livewire/livewire

Livewire assets are automatically injected. For manual control, add to your layout:

<html>
<head>
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>

Creating Components

Generate a Component

php artisan make:livewire DataTable

This creates two files:

  • app/Http/Livewire/DataTable.php - Component class
  • resources/views/livewire/data-table.blade.php - Template

Component Structure

Component class app/Http/Livewire/DataTable.php:

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\User;
class DataTable extends Component
{
use WithPagination;
// Public properties are automatically reactive.
public $search = '';
public $status = 'all';
public $sortField = 'created_at';
public $sortDirection = 'desc';
// Reset pagination when filters change.
protected $queryString = [
'search' => ['except' => ''],
'status' => ['except' => 'all'],
];
public function updatingSearch()
{
$this->resetPage();
}
public function updatingStatus()
{
$this->resetPage();
}
public function sortBy($field)
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function render()
{
$users = User::query()
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
});
})
->when($this->status !== 'all', function ($query) {
$query->where('status', $this->status);
})
->orderBy($this->sortField, $this->sortDirection)
->paginate(10);
return view('livewire.data-table', [
'users' => $users,
]);
}
}

Template resources/views/livewire/data-table.blade.php:

<div>
<div class="filters mb-4 flex gap-4">
<input
type="text"
wire:model.debounce.300ms="search"
placeholder="Search users..."
class="form-control"
>
<select wire:model="status" class="form-control">
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<table class="table">
<thead>
<tr>
<th wire:click="sortBy('name')" class="cursor-pointer">
Name
@if($sortField === 'name')
@if($sortDirection === 'asc') ↑ @else ↓ @endif
@endif
</th>
<th wire:click="sortBy('email')" class="cursor-pointer">
Email
</th>
<th wire:click="sortBy('created_at')" class="cursor-pointer">
Created
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->created_at->format('M d, Y') }}</td>
<td>
<button wire:click="edit({{ $user->id }})" class="btn btn-sm btn-primary">
Edit
</button>
<button
wire:click="delete({{ $user->id }})"
wire:confirm="Are you sure you want to delete this user?"
class="btn btn-sm btn-danger"
>
Delete
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center">No users found.</td>
</tr>
@endforelse
</tbody>
</table>
{{ $users->links() }}
</div>

Using Components

In any Blade view:

<livewire:data-table />
{{-- Or with parameters --}}
<livewire:data-table :status="'active'" />

Form Handling

Create Form Component

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Post;
class PostForm extends Component
{
public Post $post;
protected $rules = [
'post.title' => 'required|min:3|max:255',
'post.body' => 'required|min:10',
'post.published' => 'boolean',
];
protected $messages = [
'post.title.required' => 'The post title is required.',
'post.title.min' => 'The title must be at least 3 characters.',
];
public function mount(Post $post = null)
{
$this->post = $post ?? new Post();
}
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function save()
{
$this->validate();
$isNew = !$this->post->exists;
$this->post->user_id = auth()->id();
$this->post->save();
session()->flash('message', $isNew
? 'Post created successfully!'
: 'Post updated successfully!'
);
return redirect()->route('posts.index');
}
public function render()
{
return view('livewire.post-form');
}
}

Template:

<form wire:submit.prevent="save">
@if (session()->has('message'))
<div class="alert alert-success">
{{ session('message') }}
</div>
@endif
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
id="title"
wire:model="post.title"
class="form-control @error('post.title') is-invalid @enderror"
>
@error('post.title')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="body">Content</label>
<textarea
id="body"
wire:model.lazy="post.body"
rows="10"
class="form-control @error('post.body') is-invalid @enderror"
></textarea>
@error('post.body')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-check">
<input
type="checkbox"
id="published"
wire:model="post.published"
class="form-check-input"
>
<label for="published" class="form-check-label">Published</label>
</div>
<button type="submit" class="btn btn-primary" wire:loading.attr="disabled">
<span wire:loading.remove>Save Post</span>
<span wire:loading>Saving...</span>
</button>
</form>

Event Communication

Emitting Events

Components can communicate via events:

// In the child component.
public function selectItem($id)
{
$this->emit('itemSelected', $id);
}
// Emit to the parent only.
$this->emitUp('itemSelected', $id);
// Emit to a specific component.
$this->emitTo('shopping-cart', 'itemAdded', $productId);

Listening for Events

// In the parent component.
protected $listeners = [
'itemSelected' => 'handleItemSelected',
'refreshData' => '$refresh', // Special: re-render component.
];
public function handleItemSelected($id)
{
$this->selectedItem = Item::find($id);
}

JavaScript to Livewire

Emit events from JavaScript:

// Emit to all Livewire components.
Livewire.emit('filterUpdated', filterValue);
// Emit to a specific component.
Livewire.emitTo('data-table', 'filterUpdated', filterValue);

Listen in component:

protected $listeners = [
'filterUpdated' => 'setFilter',
];
public function setFilter($value)
{
$this->filter = $value;
}

Loading States

Wire Loading Directive

{{-- Show/hide elements --}}
<span wire:loading>Processing...</span>
<span wire:loading.remove>Ready</span>
{{-- Target specific actions --}}
<span wire:loading wire:target="save">Saving post...</span>
<span wire:loading wire:target="delete">Deleting...</span>
{{-- Target multiple actions --}}
<div wire:loading wire:target="save, update, delete">
Working...
</div>
{{-- CSS classes --}}
<button wire:loading.class="opacity-50" wire:target="save">
Save
</button>
{{-- Disable elements --}}
<button wire:loading.attr="disabled">Submit</button>

Loading Indicator Component

Create a reusable loading overlay:

{{-- resources/views/livewire/partials/loading.blade.php --}}
<div
wire:loading.flex
class="fixed inset-0 bg-black bg-opacity-25 items-center justify-center z-50"
>
<div class="bg-white p-4 rounded-lg shadow-lg">
<svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" />
</svg>
</div>
</div>

File Uploads

Enable File Uploads

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class ImageUpload extends Component
{
use WithFileUploads;
public $photo;
public $photos = [];
protected $rules = [
'photo' => 'image|max:1024', // 1 MB max.
'photos.*' => 'image|max:1024',
];
public function updatedPhoto()
{
$this->validate([
'photo' => 'image|max:1024',
]);
}
public function save()
{
$this->validate();
// Store a single file.
$path = $this->photo->store('photos', 'public');
// Store multiple files.
foreach ($this->photos as $photo) {
$photo->store('photos', 'public');
}
session()->flash('message', 'Photos uploaded!');
$this->reset(['photo', 'photos']);
}
public function render()
{
return view('livewire.image-upload');
}
}

Template:

<form wire:submit.prevent="save">
{{-- Single file --}}
<div class="form-group">
<label>Single Photo</label>
<input type="file" wire:model="photo">
@error('photo')
<span class="text-red-500">{{ $message }}</span>
@enderror
{{-- Preview --}}
@if ($photo)
<img src="{{ $photo->temporaryUrl() }}" class="mt-2 max-w-xs">
@endif
<div wire:loading wire:target="photo">Uploading...</div>
</div>
{{-- Multiple files --}}
<div class="form-group">
<label>Multiple Photos</label>
<input type="file" wire:model="photos" multiple>
@if ($photos)
<div class="flex gap-2 mt-2">
@foreach($photos as $photo)
<img src="{{ $photo->temporaryUrl() }}" class="w-20 h-20 object-cover">
@endforeach
</div>
@endif
</div>
<button type="submit" wire:loading.attr="disabled">
Upload Photos
</button>
</form>

Real-Time Validation

Instant Feedback

public function updated($propertyName)
{
$this->validateOnly($propertyName);
}

Debounced Input

{{-- Validate 500ms after user stops typing --}}
<input type="text" wire:model.debounce.500ms="email">
{{-- Validate only on blur --}}
<input type="text" wire:model.lazy="email">
{{-- Immediate validation (default) --}}
<input type="text" wire:model="email">

Polling and Auto-Refresh

Automatic Polling

{{-- Refresh every 2 seconds --}}
<div wire:poll.2s>
Current time: {{ now() }}
</div>
{{-- Poll only when tab is visible --}}
<div wire:poll.visible.5s>
{{ $notifications->count() }} notifications
</div>
{{-- Poll a specific method --}}
<div wire:poll.10s="refreshData">
{{ $data }}
</div>

Integration with Alpine.js

Livewire pairs excellently with Alpine.js for client-side interactivity:

<div
x-data="{ open: false }"
@item-selected.window="open = false"
>
<button @click="open = !open">Toggle Menu</button>
<div x-show="open" x-cloak>
<ul>
@foreach($items as $item)
<li>
<button wire:click="selectItem({{ $item->id }})">
{{ $item->name }}
</button>
</li>
@endforeach
</ul>
</div>
</div>

Useful Libraries

Enhance Livewire with these packages:

  • Filament: Full admin panel framework built on Livewire
  • WireUI: UI component library for Livewire
  • Volt: Single-file Livewire components
# Install Filament.
composer require filament/filament
# Install WireUI.
composer require wireui/wireui

Key Takeaways

  1. Public properties are reactive: Any public property change triggers a re-render.
  2. Use wire:model wisely: Choose between instant, debounced, or lazy updates.
  3. Leverage events: Decouple components with event-driven communication.
  4. Show loading states: Always indicate when operations are in progress.
  5. Validate in real time: Use validateOnly() for instant feedback.
  6. Combine with Alpine.js: Use Alpine for pure client-side interactions.

Livewire enables building sophisticated, reactive interfaces while keeping all logic in PHP, making it perfect for Laravel developers who want modern UIs without a JavaScript framework.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive