Livewire Best Practices
Livewire Best Practices
Overview
This guide covers best practices, patterns, and tips for building production-ready Livewire applications with UltraViolet Pro.
Component Organization
1. Keep Components Focused
Bad - Too much responsibility:
class Dashboard extends Component
{
public function users() { }
public function orders() { }
public function stats() { }
public function reports() { }
// 20 more methods...
}
Good - Single responsibility:
class UsersList extends Component { }
class OrdersTable extends Component { }
class StatsWidget extends Component { }
class ReportsChart extends Component { }
2. Use Subdirectories
Organize components by feature:
app/Livewire/
├── Dashboard/
│ ├── StatsWidget.php
│ ├── RecentOrders.php
│ └── QuickActions.php
├── Users/
│ ├── UsersList.php
│ ├── UserForm.php
│ └── UserProfile.php
└── Reports/
└── SalesReport.php
State Management
1. Only Store What You Need
Bad - Storing entire eloquent models:
class UserEdit extends Component
{
public User $user; // Entire model in state!
public function mount(User $user)
{
$this->user = $user;
}
}
Good - Store only necessary data:
class UserEdit extends Component
{
public $userId;
public $name;
public $email;
public function mount(User $user)
{
$this->userId = $user->id;
$this->name = $user->name;
$this->email = $user->email;
}
}
2. Use Computed Properties
Bad - Recalculating on every render:
public function render()
{
return view('livewire.users', [
'users' => User::where('active', true)->get(),
'stats' => $this->calculateStats(),
'summary' => $this->generateSummary()
]);
}
Good - Cache computed values:
#[Computed]
public function users()
{
return User::where('active', true)->get();
}
#[Computed]
public function stats()
{
return [
'total' => $this->users->count(),
'active' => $this->users->where('status', 'active')->count()
];
}
public function render()
{
return view('livewire.users');
}
Performance Optimization
1. Lazy Load Heavy Components
<!-- Load immediately -->
<livewire:stats-widget />
<!-- Load after initial page render -->
<livewire:heavy-chart lazy />
2. Debounce Search Inputs
Bad - Updates on every keystroke:
<input wire:model.live="search">
Good - Debounced updates:
<input wire:model.live.debounce.500ms="search">
3. Use wire:key for Dynamic Lists
Bad - No keys:
@foreach($items as $item)
<div>{{ $item->name }}</div>
@endforeach
Good - With keys:
@foreach($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
4. Optimize Database Queries
Bad - N+1 queries:
public function render()
{
return view('livewire.posts', [
'posts' => Post::all() // Then accessing $post->user in view
]);
}
Good - Eager loading:
public function render()
{
return view('livewire.posts', [
'posts' => Post::with('user', 'comments')->limit(10)->get()
]);
}
5. Avoid Loading All Records
Bad:
'users' => User::all()
Good:
'users' => User::limit(50)->get()
// Or use pagination
'users' => User::paginate(25)
Validation
1. Real-Time Validation
class ContactForm extends Component
{
public $email;
protected $rules = [
'email' => 'required|email'
];
// Validate on blur
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function submit()
{
$this->validate();
// Submit logic
}
}
2. Custom Error Messages
protected $messages = [
'email.required' => 'Email is required.',
'email.email' => 'Please enter a valid email address.',
];
3. Conditional Validation
public function rules()
{
return [
'email' => $this->isRequired ? 'required|email' : 'nullable|email',
'phone' => 'required_if:contact_method,phone'
];
}
Security
1. Always Authorize Actions
Bad - No authorization:
public function delete($id)
{
User::find($id)->delete();
}
Good - With authorization:
public function delete($id)
{
$user = User::findOrFail($id);
$this->authorize('delete', $user);
$user->delete();
session()->flash('message', 'User deleted successfully.');
}
2. Use Policies
// UserPolicy.php
public function update(User $user, User $model)
{
return $user->id === $model->id || $user->isAdmin();
}
// In Component
public function update()
{
$user = User::find($this->userId);
$this->authorize('update', $user);
$user->update([...]);
}
3. Validate File Uploads
use Livewire\WithFileUploads;
class FileUpload extends Component
{
use WithFileUploads;
public $document;
public function save()
{
$this->validate([
'document' => 'required|file|max:10240|mimes:pdf,doc,docx'
]);
$path = $this->document->store('documents', 'private');
}
}
Error Handling
1. Graceful Error Handling
public function save()
{
try {
$this->validate();
// Save logic
User::create([...]);
session()->flash('message', 'Success!');
session()->flash('type', 'success');
} catch (\Exception $e) {
Log::error('Save failed: ' . $e->getMessage());
session()->flash('message', 'An error occurred. Please try again.');
session()->flash('type', 'error');
}
}
2. User-Friendly Messages
Bad:
session()->flash('message', $e->getMessage()); // Technical error
Good:
session()->flash('message', 'Unable to save user. Please check your inputs and try again.');
Log::error('User save error', ['exception' => $e, 'user_id' => $this->userId]);
UX Best Practices
1. Loading States
<button wire:click="save">
<span wire:loading.remove wire:target="save">Save</span>
<span wire:loading wire:target="save">
<span class="spinner-border spinner-border-sm"></span>
Saving...
</span>
</button>
2. Disable During Loading
<button wire:click="save" wire:loading.attr="disabled">
Save
</button>
3. Optimistic Updates
public function toggleComplete($taskId)
{
// Update UI immediately (optimistic)
$task = $this->tasks->firstWhere('id', $taskId);
$task->completed = !$task->completed;
// Then save to database
Task::find($taskId)->update([
'completed' => $task->completed
]);
}
4. Confirmation Dialogs
<button
wire:click="delete({{ $user->id }})"
wire:confirm="Are you sure you want to delete this user?"
>
Delete
</button>
Testing
1. Unit Tests for Components
use Livewire\Livewire;
test('can create user', function () {
Livewire::test(UserCreate::class)
->set('name', 'John Doe')
->set('email', 'john@example.com')
->call('save')
->assertDispatched('user-created')
->assertHasNoErrors();
expect(User::where('email', 'john@example.com')->exists())->toBeTrue();
});
2. Test Validation
test('validates required fields', function () {
Livewire::test(UserCreate::class)
->set('email', 'invalid-email')
->call('save')
->assertHasErrors(['email']);
});
3. Test Authorization
test('unauthorized user cannot delete', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
actingAs($user);
Livewire::test(UserDelete::class)
->call('delete', $otherUser->id)
->assertForbidden();
});
Common Patterns
1. Modal Form
class UserModal extends Component
{
public $showModal = false;
public $userId;
public $name;
#[On('edit-user')]
public function edit($userId)
{
$user = User::find($userId);
$this->userId = $user->id;
$this->name = $user->name;
$this->showModal = true;
}
public function save()
{
$this->validate(['name' => 'required']);
User::find($this->userId)->update([
'name' => $this->name
]);
$this->showModal = false;
$this->dispatch('user-updated');
}
}
2. Search with Filters
class UserSearch extends Component
{
public $search = '';
public $status = 'all';
public $perPage = 10;
public function render()
{
$query = User::query();
if ($this->search) {
$query->where('name', 'like', "%{$this->search}%");
}
if ($this->status !== 'all') {
$query->where('status', $this->status);
}
return view('livewire.user-search', [
'users' => $query->paginate($this->perPage)
]);
}
}
3. Inline Editing
class InlineEdit extends Component
{
public $editing = false;
public $userId;
public $name;
public function edit()
{
$this->editing = true;
}
public function save()
{
$this->validate(['name' => 'required']);
User::find($this->userId)->update(['name' => $this->name]);
$this->editing = false;
}
public function cancel()
{
$this->editing = false;
$this->name = User::find($this->userId)->name;
}
}
Common Pitfalls
1. Don't Store Models Directly
❌ Wrong:
public User $user;
✅ Correct:
public $userId;
public function getUser()
{
return User::find($this->userId);
}
2. Don't Use Closures in Properties
❌ Wrong:
public $callback;
public function mount()
{
$this->callback = function() { /* ... */ };
}
✅ Correct:
public function executeCallback()
{
// Logic here
}
3. Don't Forget wire:key in Loops
❌ Wrong:
@foreach($items as $item)
<div>{{ $item->name }}</div>
@endforeach
✅ Correct:
@foreach($items as $item)
<div wire:key="item-{{ $item->id }}">{{ $item->name }}</div>
@endforeach
4. Don't Overuse Real-Time Updates
❌ Wrong - Every keystroke triggers update:
<input wire:model.live="search">
✅ Better - Debounced:
<input wire:model.live.debounce.500ms="search">
✅ Best - On blur:
<input wire:model.blur="name">
Debugging
1. Use Livewire DevTools
Install the browser extension:
- Chrome: Livewire DevTools
- Firefox: Livewire DevTools
2. Debug Specific Components
public function render()
{
dd($this->all()); // Dump component state
return view('livewire.component');
}
3. Log Livewire Requests
// In component
public function updated($name, $value)
{
Log::info("Property {$name} updated", ['value' => $value]);
}
4. Check Network Tab
Open browser DevTools → Network → Filter by "livewire/update"
Look for:
- Request payload
- Response data
- Errors in response
Accessibility
1. Use Proper ARIA Attributes
<button
wire:click="delete"
aria-label="Delete user"
wire:loading.attr="aria-busy=true"
>
<i class="material-symbols-rounded">delete</i>
</button>
2. Keyboard Navigation
<div
tabindex="0"
wire:click="select"
@keydown.enter="$wire.select()"
>
Selectable item
</div>
3. Loading Announcements
<div
role="status"
aria-live="polite"
wire:loading
>
Loading content...
</div>
Deployment Checklist
Before deploying Livewire apps:
- [ ] Run
npm run buildfor production assets - [ ] Set
APP_ENV=productionin.env - [ ] Enable caching:
php artisan config:cache - [ ] Enable route caching:
php artisan route:cache - [ ] Enable view caching:
php artisan view:cache - [ ] Set
LIVEWIRE_ASSET_URLif using CDN - [ ] Test all components in production mode
- [ ] Monitor Livewire requests in production
- [ ] Set up error tracking (Sentry, Bugsnag, etc.)
Resources
- Official Docs: livewire.laravel.com
- Livewire Tips: laravellivewire-tips.com
- Community: Livewire Discord
- UltraViolet Components:
/livewire/dashboard
Summary
Key Takeaways:
- ✅ Keep components focused and single-purpose
- ✅ Optimize database queries and state management
- ✅ Always authorize actions and validate inputs
- ✅ Provide loading states and user feedback
- ✅ Use debouncing for search/filter inputs
- ✅ Add wire:key to dynamic lists
- ✅ Test your components thoroughly
- ✅ Follow accessibility best practices
Remember: Livewire makes building reactive interfaces simple, but great UX still requires thoughtful design and implementation.
Ready to build? Check out the [Components Guide]({{ route('docs.show', 'livewire/components') }}) for real-world examples! 🚀