Introduction

Livewire lets you build reactive interfaces in PHP — your Blade templates update in real time through AJAX round trips without writing a single line of JavaScript.

Key Concepts

  • Livewire component: A PHP class + Blade view pair that encapsulates state and behavior.
  • wire:click: Directive that calls a public method on the component class when an element is clicked.
  • wire:model: Two-way data binding between a form input and a public property, with modifiers like .live, .debounce, and .blur.
  • Action: A public method on the component class that can be invoked from the browser via wire:click, form submission, or keyboard events.
  • Loading state: The period between an action firing and its response arriving; Livewire exposes it via wire:loading and related modifiers.

Real World Context

When you need reactive UI (search-as-you-type, live updating lists, modal forms) but don't want to build a separate SPA with its own router and state store, Livewire is the quickest path — you stay in PHP and Blade the whole time.

Deep Dive

Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving the comfort of Laravel and Blade.

What is Livewire?

Livewire lets you build reactive, dynamic interfaces using PHP instead of JavaScript. Your Blade templates update in real-time through AJAX without writing a single line of JavaScript.

┌───────────────────────────────────────────────────────────┐
│                       Browser                              │
├───────────────────────────────────────────────────────────┤
│  ┌─────────────────┐                                      │
│  │ Livewire        │  1. User clicks button               │
│  │ Component       │  ─────────────────────►              │
│  │ (Blade + PHP)   │                                      │
│  │                 │  4. DOM updates automatically        │
│  │                 │  ◄─────────────────────              │
│  └─────────────────┘                                      │
└───────────────────────────────────────────────────────────┘
          │                         ▲
          │ 2. AJAX request         │ 3. Server returns
          │    with action          │    updated HTML
          ▼                         │
┌───────────────────────────────────────────────────────────┐
│                     Laravel Server                         │
│  ┌─────────────────┐                                      │
│  │ Livewire        │                                      │
│  │ Component Class │                                      │
│  │ (PHP)           │                                      │
│  └─────────────────┘                                      │
└───────────────────────────────────────────────────────────┘

Installation

bash
composer require livewire/livewire

Modern Livewire (v3 and the v4 shipped with Laravel 13) automatically injects its styles and scripts into any page that contains a Livewire component — you no longer need the @livewireStyles and @livewireScripts directives that Livewire v2 required. Just add a Livewire component and you are done:

blade
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    <livewire:counter />
</body>
</html>

Creating a Component

bash
php artisan make:livewire Counter

This creates:

  • app/Livewire/Counter.php - Component class
  • resources/views/livewire/counter.blade.php - Blade view
php
<?php

namespace App\Livewire;

use Livewire\Component;

class Counter extends Component
{
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }

    public function decrement(): void
    {
        $this->count--;
    }

    public function render()
    {
        return view('livewire.counter');
    }
}
blade
<!-- resources/views/livewire/counter.blade.php -->
<div>
    <button wire:click="decrement">-</button>
    <span>{{ $count }}</span>
    <button wire:click="increment">+</button>
</div>

Using Components

Include anywhere in Blade:

blade
<livewire:counter />

{{-- Or with Blade component syntax --}}
@livewire('counter')

{{-- Pass initial values --}}
<livewire:counter :count="5" />

Data Binding

Two-way bind form inputs with wire:model:

php
class SearchUsers extends Component
{
    public string $search = '';
    public array $users = [];

    public function updatedSearch(): void
    {
        $this->users = User::where('name', 'like', '%' . $this->search . '%')
            ->take(10)
            ->get()
            ->toArray();
    }

    public function render()
    {
        return view('livewire.search-users');
    }
}
blade
<div>
    <input type="text" wire:model.live="search" placeholder="Search users...">

    <ul>
        @foreach ($users as $user)
            <li>{{ $user['name'] }}</li>
        @endforeach
    </ul>
</div>

Model Modifiers

blade
{{-- Update on input (live) --}}
<input wire:model.live="search">

{{-- Debounce live updates --}}
<input wire:model.live.debounce.300ms="search">

{{-- Update on blur --}}
<input wire:model.blur="email">

{{-- Update on change (select, checkbox) --}}
<select wire:model.change="country">

Actions

Call PHP methods from the frontend:

php
class TodoList extends Component
{
    public array $todos = [];
    public string $newTodo = '';

    public function addTodo(): void
    {
        if (empty($this->newTodo)) return;

        $this->todos[] = [
            'id' => uniqid(),
            'text' => $this->newTodo,
            'completed' => false,
        ];

        $this->newTodo = '';
    }

    public function toggleTodo(string $id): void
    {
        foreach ($this->todos as &$todo) {
            if ($todo['id'] === $id) {
                $todo['completed'] = !$todo['completed'];
            }
        }
    }

    public function deleteTodo(string $id): void
    {
        $this->todos = array_filter(
            $this->todos,
            fn ($todo) => $todo['id'] !== $id
        );
    }

    public function render()
    {
        return view('livewire.todo-list');
    }
}
blade
<div>
    <form wire:submit="addTodo">
        <input type="text" wire:model="newTodo" placeholder="New todo...">
        <button type="submit">Add</button>
    </form>

    <ul>
        @foreach ($todos as $todo)
            <li>
                <input
                    type="checkbox"
                    wire:click="toggleTodo('{{ $todo['id'] }}')"
                    @checked($todo['completed'])
                >
                <span class="{{ $todo['completed'] ? 'line-through' : '' }}">
                    {{ $todo['text'] }}
                </span>
                <button wire:click="deleteTodo('{{ $todo['id'] }}')">×</button>
            </li>
        @endforeach
    </ul>
</div>

Loading States

blade
<button wire:click="save">
    <span wire:loading.remove>Save</span>
    <span wire:loading>Saving...</span>
</button>

{{-- Target specific action --}}
<span wire:loading wire:target="save">Saving...</span>

{{-- Loading class --}}
<button wire:loading.class="opacity-50" wire:click="save">Save</button>

{{-- Disable while loading --}}
<button wire:loading.attr="disabled" wire:click="save">Save</button>

Common Pitfalls

  1. Forgetting Livewire round-trips to the server — Every wire:click is a network request. It feels instant locally and painful on 3G.
  2. Binding expensive properties with wire:model.live — Live binding fires on every keystroke. Debounce or switch to .blur for fields with validation or remote lookups.
  3. Blocking the UI during long actions — Without a loading indicator, users will click twice and submit twice.

Best Practices

  1. Use .debounce or .blur to reduce network chatter — wire:model.live.debounce.300ms is a sane default for search fields.
  2. Show loading states with wire:loading — Users need feedback that something is happening.
  3. Keep components focused — A kitchen-sink component with 20 public properties is hard to reason about. Split by feature.

Summary

  • A Livewire component is a PHP class + Blade view; state lives on public properties.
  • wire:click calls methods; wire:model does two-way binding.
  • Modifiers (.live, .debounce, .blur, .change) control when the server is contacted.
  • wire:loading and its variants manage loading state.
  • Livewire 4 (Laravel 13's default) auto-injects its scripts — no more @livewireStyles/@livewireScripts.
✓ Completed