Livewire 4 — vollständiger Leitfaden

Livewire 4 behält das bekannte Komponentenmodell und baut alles darum herum neu: Single-File-Komponenten, Islands mit unabhängigem Rendern, Slots im Kontext des Elternteils, eingebautes Drag and Drop und clientseitige Direktiven, die den Server gar nicht erst behelligen. Dieser Leitfaden deckt das Framework vollständig ab und benennt die Unterschiede zu Version 3 dort, wo sie wehtun.

Livewire 4.x Laravel 11 / 12 PHP 8.2+ 26 Kapitel

1. Was Livewire 4 ändert

Livewire 4 ist das größte Release in der Geschichte des Frameworks. Das Komponentenmodell selbst hat sich nicht geändert: PHP-Klasse, Blade-Vorlage, Zustand auf dem Server, Markup über die Leitung. Geändert hat sich alles drumherum: wo Komponenten liegen, wie sie geschrieben werden und wie viel einer Seite ein einzelnes Update berührt.

Die fünf wichtigsten Punkte

NeuerungWas sie bringt
Single-File-Komponenten Klasse und Markup in einer .blade.php. Kein Springen mehr zwischen zwei Verzeichnissen für eine Komponente mit zwanzig Zeilen.
Islands Isolierte Bereiche innerhalb einer Komponente, die sich eigenständig neu rendern. Ein Umsatzzähler stößt nicht länger die Abfragen des gesamten Dashboards an.
Slots Eltern reichen Markup in Kinder hinein, ausgewertet im Kontext des Elternteils: wire:click im Slot ruft die Methode des Elternteils auf.
Optimistisches UI wire:show, wire:text, wire:bind ändern das DOM sofort, ohne Serveraufruf.
Drag and Drop wire:sort ist eingebaut. Kein SortableJS, kein Klebecode.

Ist das ein Neuschreiben der Anwendung?

Nein. Livewire 4 bleibt weitgehend abwärtskompatibel: klassenbasierte Komponenten funktionieren weiter. Single-File ist der Standard für neue Komponenten, keine erzwungene Migration. Es gibt eine Reihe von Breaking Changes, die man vor dem Upgrade lesen sollte — sie sind im Vergleich Livewire 3 und 4 gesammelt, und der offizielle Upgrade Guide bleibt die maßgebliche Quelle.

Sie kommen von Livewire 3? Das eigentlich Neue steckt in den Kapiteln 3, 6, 10 und 11: Single-File-Komponenten, die neue wire:model-Semantik, Islands und Slots. Der Rest des Komponentenmodells wird vertraut wirken.

2. Installation und Voraussetzungen

Bash
composer require livewire/livewire:^4.0

php artisan optimize:clear

Wie in Version 3 werden die Assets automatisch eingebunden. @livewireStyles und @livewireScripts müssen Sie nur bei einem abweichenden Layout oder strenger Content Security Policy von Hand setzen.

Die Livewire-Endpunkte haben sich geändert. Update-URLs enthalten jetzt einen Hash: aus /livewire/ wurde /livewire-{hash}/. Firewall-Regeln, WAF-Ausnahmen, CDN-Bypässe oder nginx-Location-Blöcke, die auf den wörtlichen Pfad /livewire/ zeigen, müssen auf das neue Muster erweitert werden — sonst wirkt die Anwendung in der Produktion kaputt, während sie auf dem Laptop einwandfrei läuft.

Konfiguration veröffentlichen

Bash
php artisan livewire:publish --config
EinstellungZweck
component_locationsVerzeichnisse, die nach Komponenten durchsucht werden. Standard: resources/views/components und resources/views/livewire.
component_namespacesBenannte Wurzeln, etwa pages::.
component_layoutHieß in v3 layout. Nutzt den Namespace layouts::.
component_placeholderHieß in v3 lazy_placeholder.
make_commandWas make:livewire erzeugt: Single-File oder klassenbasiert.
smart_wire_keysJetzt standardmäßig true.
csp_safeBuild, der strengere CSP erfüllt.
config/livewire.php
// Weiterhin klassenbasierte Komponenten erzeugen
'make_command' => [
    'type' => 'class',
],

3. Single-File-Komponenten

Die auffälligste Änderung im Alltag. Eine Komponente ist jetzt eine Blade-Datei, die mit einem PHP-Block samt anonymer Klasse beginnt und danach das Markup enthält.

Bash
php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php
resources/views/components/post/⚡create.blade.php
<?php

use Livewire\Component;
use App\Models\Post;
use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|string|min:5|max:180')]
    public string $title = '';

    #[Validate('required|string|min:50')]
    public string $body = '';

    public function save()
    {
        $this->validate();

        Post::create([
            'title'   => $this->title,
            'body'    => $this->body,
            'user_id' => auth()->id(),
        ]);

        $this->reset();

        $this->dispatch('notify', message: 'Beitrag erstellt');
    }
};

?>

<div>
    <form wire:submit="save">
        <input type="text" wire:model="title" placeholder="Titel">
        @error('title') <span class="error">{{ $message }}</span> @enderror

        <textarea wire:model="body" placeholder="Text"></textarea>
        @error('body') <span class="error">{{ $message }}</span> @enderror

        <button type="submit">Speichern</button>
    </form>
</div>

Öffentliche Eigenschaften stehen im Markup als gewöhnliche Variablen bereit — {{ $title }} funktioniert genauso wie zuvor in einer separaten Vorlagendatei.

Zu dem Emoji

Der Standarddateiname trägt das Präfix . Es ist eine optische Markierung, die Livewire- von gewöhnlichen Blade-Komponenten im selben Verzeichnis unterscheidet, und sie ist optional — in config/livewire.php lässt sie sich abschalten.

Multi-File-Komponenten

Bash
php artisan make:livewire post.create --mfc

Klassenbasierte Komponenten funktionieren weiter

app/Livewire/CreatePost.php
<?php

namespace App\Livewire;

use Livewire\Component;

class CreatePost extends Component
{
    public string $title = '';

    public function render()
    {
        return view('livewire.create-post');
    }
}
Was wählen. Single-File gewinnt bei kleinen und mittleren Komponenten — ein Formular, eine Tabellenzeile, ein Widget. Klassenbasiert oder Multi-File ist besser, wenn die Klasse echte Logik und viele Abhängigkeiten trägt.

4. Ablage, Benennung und Namespaces

Verzeichnisstruktur
resources/views/
├── components/
│   ├── ⚡counter.blade.php              → <livewire:counter />
│   ├── post/
│   │   ├── ⚡create.blade.php           → <livewire:post.create />
│   │   └── ⚡index.blade.php            → <livewire:post.index />
│   └── button.blade.php                 (gewöhnliche Blade-Komponente)
└── pages/
    └── ⚡dashboard.blade.php            → <livewire:pages::dashboard />
Blade
{{-- Nach Name --}}
<livewire:counter />

{{-- Verschachtelte Verzeichnisse mit Punkten --}}
<livewire:post.create />

{{-- Mit Namespace --}}
<livewire:pages::post.create />

{{-- Mit Props --}}
<livewire:post.create :title="$initialTitle" :author="$user" />
Komponenten-Tags müssen geschlossen werden. In v4 rendert ein nicht geschlossenes <livewire:some-component> schlicht gar nichts — keine Fehlermeldung, keine Ausgabe. Immer selbstschließend notieren: <livewire:some-component />. Das ist die häufigste Überraschung beim Übernehmen von Blade-Dateien aus v3.

Routing auf eine Komponente

routes/web.php
use App\Livewire\Dashboard;

// Nach Klasse
Route::livewire('/dashboard', Dashboard::class);

// Nach Komponentenname
Route::livewire('/dashboard', 'pages::dashboard')
    ->middleware('auth')
    ->name('dashboard');

Layout

config/livewire.php
'component_layout' => 'layouts::app',
PHP
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;

new class extends Component {
    #[Layout('layouts::app')]
    #[Title('Übersicht')]
    public function render() { /* … */ }
};

5. Eigenschaften und Props

PHP
new class extends Component {
    public string $name = '';
    public ?int $age = null;
    public array $tags = [];
    public bool $isPublic = false;
};

Props vom Elternteil: #[Prop]

resources/views/components/⚡alert.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $type = 'info';

    #[Prop]
    public bool $dismissible = false;
};

?>

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $slot }}

    @if ($dismissible)
        <button wire:click="$dispatch('dismiss')">×</button>
    @endif
</div>
Blade
<livewire:alert type="warning" dismissible class="mt-4">
    Die Rechnung ist überfällig.
</livewire:alert>

mount() läuft weiterhin zuerst

PHP
use App\Models\Order;

new class extends Component {
    public Order $order;
    public string $mode;

    public function mount(Order $order, string $mode = 'compact'): void
    {
        $this->order = $order;
        $this->mode  = $mode;
    }
};

#[Locked] ist genauso wichtig

PHP
use Livewire\Attributes\Locked;

new class extends Component {
    #[Locked]
    public int $invoiceId;   // der Client kann keine fremde id unterschieben

    public string $note = '';
};
Version 4 ändert nichts am Bedrohungsmodell: jede öffentliche Eigenschaft ist vom Client beschreibbar, solange Sie sie nicht sperren, und jede öffentliche Methode ist ein erreichbarer Endpunkt. Siehe Kapitel 25.

6. wire:model in Version 4

Diese Änderung erwischt Sie am ehesten. Zwei Aspekte von wire:model verhalten sich anders, und beide scheitern lautlos statt mit einer Fehlermeldung.

Änderung 1: Modifikatoren steuern die clientseitige Synchronisation

In v3 entschieden .blur und .change nur, wann eine Netzwerkanfrage ausgelöst wird. In v4 entscheiden sie, wann der Wert überhaupt synchronisiert — auch auf dem Client. Für das alte Verhalten ergänzen Sie .live.

Blade
{{-- Livewire 3 --}}
<input wire:model.blur="title">

{{-- Entspricht in v4 dem gleichen Verhalten --}}
<input wire:model.live.blur="title">

Änderung 2: kein Event-Bubbling von Kindelementen mehr

Blade
{{-- Livewire 3: der Wrapper fing das Event des Feldes ab --}}
<div wire:model="value">
    <input type="text">
</div>

{{-- Livewire 4: ausdrücklich aktivieren --}}
<div wire:model.deep="value">
    <input type="text">
</div>

Der Rest bleibt gleich

Blade
{{-- Standardmäßig verzögert --}}
<input type="text" wire:model="name">

{{-- Anfrage bei jeder Änderung --}}
<input type="text" wire:model.live="search">

{{-- Live-Suche mit Verzögerung --}}
<input type="text" wire:model.live.debounce.400ms="search">

{{-- Verschachtelte Daten --}}
<input wire:model="form.address.city">
<input wire:model="post.title">
Die Bindung an eine Modelleigenschaft (post.title) verlangt weiterhin eine passende Validierungsregel. Das ist der Schutz vor Mass Assignment und ist unverändert nach v4 übernommen.

7. Aktionen, Async und Renderless

Blade
<button wire:click="save">Speichern</button>

<form wire:submit="save">…</form>

<button wire:click="delete({{ $post->id }})"
        wire:confirm="Beitrag löschen? Das lässt sich nicht rückgängig machen.">Löschen</button>

Aktionen ohne Neurendern

PHP
use Livewire\Attributes\Renderless;

new class extends Component {
    #[Renderless]
    public function incrementViewCount(): void
    {
        $this->post->increment('views');
    }
};
Blade
<button type="button" wire:click.renderless="incrementViewCount">Zählen</button>

Asynchrone Aktionen

#[Async] (oder der Modifikator .async) führt eine Aktion parallel aus, außerhalb der normalen Anfrage-Warteschlange. Ideal für Fire-and-forget: Analytics, Logging, Cache-Vorwärmung.

PHP
use Livewire\Attributes\Async;

new class extends Component {
    #[Async]
    public function logInteraction(string $element): void
    {
        Analytics::record($element, auth()->id());
    }
};
Blade
<button wire:click.async="logInteraction('cta-hero')">Loslegen</button>
Niemals async für Aktionen verwenden, die im UI sichtbaren Zustand ändern. Da sie die Warteschlange umgehen, kann ihr Ergebnis relativ zu anderen Updates in falscher Reihenfolge eintreffen — und der Zustand widerspricht dem Bildschirm. Async ist für Seiteneffekte, die die Oberfläche nicht zurückliest.
Blade
<button wire:click="$refresh">Aktualisieren</button>
<button wire:click="$set('tab', 'settings')">Einstellungen</button>
<button wire:click="$toggle('showFilters')">Filter</button>
<button wire:click="$parent.closeModal()">Schließen</button>

8. Lebenszyklus und Hooks

Erstes Rendern
1. Komponenteninstanz wird erzeugt
2. boot()
3. mount($params)
4. booted()
5. render()
Folgeaktualisierung
 1. POST /livewire-{hash}/update trifft mit dem Zustands-Snapshot ein
 2. Prüfsumme des Snapshots wird verifiziert
 3. Komponenteninstanz wird erzeugt
 4. boot()
 5. Zustand wird aus dem Snapshot wiederhergestellt
 6. hydrate() / hydrateFoo()
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)
 9. Eigenschaften erhalten neue Werte
10. updated($prop, $value) / updatedFoo($value)
11. Aktionen aus der Warteschlange laufen
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — Zustand wird zurück in den Snapshot gepackt
14. Antwort: Markup + effects
PHP
new class extends Component {
    public float $price = 0;

    public function boot(): void {}
    public function booted(): void {}
    public function hydrate(): void {}
    public function dehydrate(): void {}

    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }
};
Islands ändern den Umfang des Renderns, nicht die Reihenfolge dieser Hooks. Ein Island-Update durchläuft weiterhin den vollen Serverzyklus — es liefert nur ein Fragment statt der ganzen Komponente.

9. Validierung und Form-Objekte

PHP
use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|string|min:2|max:80')]
    public string $name = '';

    #[Validate('required|email:rfc,dns')]
    public string $email = '';

    #[Validate('accepted', message: 'Die Einwilligung zur Datenverarbeitung ist erforderlich.')]
    public bool $consent = false;

    public function submit(): void
    {
        $data = $this->validate();

        Contact::create($data);

        $this->reset();
    }
};
PHP
protected function rules(): array
{
    return [
        'email' => [
            'required',
            'email',
            Rule::unique('users', 'email')->ignore($this->userId),
        ],
        'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
    ];
}

Form-Objekte

app/Livewire/Forms/PostForm.php
<?php

namespace App\Livewire\Forms;

use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    public ?Post $post = null;

    #[Validate('required|string|min:5|max:180')]
    public string $title = '';

    #[Validate('required|string|min:50')]
    public string $body = '';

    public function store(): Post
    {
        $this->validate();

        return Post::create($this->except('post'));
    }

    public function update(): void
    {
        $this->validate();

        $this->post->update($this->except('post'));
    }
}

Fehler in JavaScript

Blade
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>

10. Islands

Islands sind das Aushängeschild von Livewire 4 und die Neuerung, die das Denken über Komponentengröße verändert. Ein Island ist ein Bereich innerhalb einer Komponente, der sich unabhängig neu rendert: bei einem Update wird nur dieses Fragment neu berechnet und gepatcht, nicht die ganze Komponente.

Blade
@island
    <div>Umsatz: {{ $this->revenue }}</div>
@endisland

Warum das zählt

In v3 war das Standardmittel gegen „dieses Dashboard ist langsam“ die Aufteilung in sechs Kindkomponenten, damit ein Widget-Update nicht die Abfragen der anderen fünf erneut ausführt. Islands liefern dieselbe Isolation ohne Komponentengrenze: eine Komponente, eine Klasse, mehrere unabhängig aktualisierende Bereiche.

Dashboard mit drei Islands
<?php

use Livewire\Component;
use Livewire\Attributes\Computed;

new class extends Component {
    #[Computed]
    public function revenue()
    {
        return Order::whereMonth('created_at', now()->month)->sum('total');
    }

    #[Computed]
    public function queue()
    {
        return Job::pending()->count();
    }
};

?>

<div>
    @island(name: 'revenue')
        <div class="card">
            <h3>Umsatz diesen Monat</h3>
            <p class="stat">{{ number_format($this->revenue, 2, ',', '.') }} €</p>
        </div>
    @endisland

    @island(name: 'queue', poll: '5s')
        <div class="card">
            <h3>Offene Jobs</h3>
            <p class="stat">{{ $this->queue }}</p>
        </div>
    @endisland

    @island(name: 'feed', lazy: true)
        @placeholder
            <div class="card skeleton animate-pulse h-64"></div>
        @endplaceholder

        <div class="card">…</div>
    @endisland
</div>

Optionen

OptionWirkung
nameBenennt das Island, damit Aktionen und JavaScript es adressieren können. Mehrere Islands mit gleichem Namen rendern immer als Gruppe.
lazyRendert, sobald das Island in den Viewport scrollt.
deferRendert direkt nach dem Laden der Seite, unabhängig von der Sichtbarkeit.
alwaysErzwingt ein Update bei jedem Rendern des Elternteils.
skipÜberspringt das initiale Rendern.

Ein Island aus einer Aktion adressieren

Blade
@island(name: 'revenue')
    Umsatz: {{ $this->revenue }}
@endisland

<button wire:click="$refresh" wire:island="revenue">Umsatz aktualisieren</button>

Append und Prepend — Endlosliste ohne Verkabelung

Blade
@island(name: 'feed')
    @foreach ($this->posts as $post)
        <article wire:key="post-{{ $post->id }}">{{ $post->title }}</article>
    @endforeach
@endisland

<button wire:click="loadMore" wire:island.append="feed">Mehr laden</button>
Blade
<button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
    Mehr laden
</button>

Polling auf ein Island beschränkt

Blade
@island(name: 'queue')
    <div wire:poll.3s>
        Offene Jobs: {{ $this->queue }}
    </div>
@endisland
Island oder Kindkomponente. Ein Island, wenn der Bereich den Zustand des Elternteils nutzt und nur isoliertes Rendern braucht. Eine Kindkomponente, wenn der Bereich eigenen Zustand, einen eigenen Lebenszyklus oder Wiederverwendung an mehreren Stellen braucht.

11. Slots und Attribut-Weitergabe

Livewire-Komponenten nehmen jetzt Slot-Inhalte entgegen, wie Blade-Komponenten es immer konnten — mit einer wichtigen Besonderheit: Slot-Inhalt wird im Kontext des Elternteils ausgewertet. Ein wire:click im Slot ruft die Methode des Elternteils auf, nicht die des Kindes.

Standard-Slot

Eltern-Vorlage
<livewire:modal>
    <h2>Beitrag erstellen</h2>

    <form wire:submit="save">
        <input wire:model="title">
        <button type="submit">Speichern</button>
    </form>
</livewire:modal>
resources/views/components/⚡modal.blade.php
<?php

use Livewire\Component;

new class extends Component {
    public bool $isOpen = false;

    public function toggle(): void
    {
        $this->isOpen = ! $this->isOpen;
    }
};

?>

<div wire:show="isOpen" class="modal">
    <button wire:click="toggle" class="modal__close">×</button>

    <div class="modal__body">
        {{ $slot }}
    </div>
</div>

Benannte Slots

Eltern-Vorlage
<livewire:modal>
    <wire:slot name="header">
        <h2>Beitrag erstellen</h2>
    </wire:slot>

    <form wire:submit="save">
        <input wire:model="title">
    </form>

    <wire:slot name="footer">
        <button wire:click="save">Speichern</button>
    </wire:slot>
</livewire:modal>
Innerhalb der Komponente
<div class="modal">
    @if ($header = $slot('header'))
        <div class="modal__header">{{ $header }}</div>
    @endif

    <div class="modal__body">{{ $slot }}</div>

    @if ($footer = $slot('footer'))
        <div class="modal__footer">{{ $footer }}</div>
    @endif
</div>

Attribut-Weitergabe

resources/views/components/⚡alert.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $type = 'info';
};

?>

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $slot }}
</div>
Blade
<livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
    Diese Rechnung ist 14 Tage überfällig.
</livewire:alert>

12. Verschachtelte Komponenten und wire:key

Blade
<div>
    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>

Smart Wire Keys

In Version 4 ist smart_wire_keys standardmäßig aktiv, sodass Livewire Keys in vielen Schleifensituationen selbst ableitet, in denen zuvor ein expliziter nötig war.

Schreiben Sie Keys weiterhin, wo Identität zählt. Jede Liste, die sortiert, gefiltert oder teilweise gelöscht werden kann, sollte einen expliziten wire:key mit der Datensatz-id tragen.
Blade
@foreach ($rows as $row)
    <div wire:key="row-{{ $row->id }}">…</div>
@endforeach
PHP
use Livewire\Attributes\Reactive;

new class extends Component {
    #[Reactive]
    public int $quantity;
};
Blade
<button wire:click="$parent.refreshList()">Liste aktualisieren</button>
PHP
use Livewire\Attributes\Modelable;

new class extends Component {
    #[Modelable]
    public int $value = 0;
};
Mit Islands greifen Sie seltener zur Kindkomponente. War der einzige Grund für die Aufteilung „damit es sich eigenständig neu rendert“, ist ein Island leichter.

13. Events

Die Event-API ist gegenüber Version 3 unverändert.

PHP
$this->dispatch('post-created');
$this->dispatch('post-created', postId: $post->id, title: $post->title);
$this->dispatch('refresh')->to(OrderList::class);
$this->dispatch('recalculate')->self();
PHP
use Livewire\Attributes\On;

new class extends Component {
    #[On('post-created')]
    public function onPostCreated(int $postId, string $title): void
    {
        // …
    }
};
PHP
$this->dispatch('notify', type: 'success', message: 'Bestellung gespeichert');
Blade
<div x-data="{ show: false, message: '' }"
     x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
     x-show="show"
     x-transition
     class="toast">
    <span x-text="message"></span>
</div>
Islands ersetzen oft Events. Das klassische v3-Muster „Komponente A dispatcht, Komponente B lauscht und aktualisiert sich“ wird häufig zu „beide Bereiche sind Islands einer Komponente, und die Aktion adressiert das andere Island“ — eine Anfrage statt zwei.

14. Berechnete Eigenschaften

PHP
use Livewire\Attributes\Computed;

new class extends Component {
    public array $items = [];

    #[Computed]
    public function products()
    {
        return Product::whereIn('id', array_keys($this->items))->get();
    }

    #[Computed]
    public function subtotal(): float
    {
        return $this->products->sum(
            fn ($product) => $product->price * $this->items[$product->id]
        );
    }
};
Blade
@island(name: 'totals')
    <p>Zwischensumme: {{ number_format($this->subtotal, 2, ',', '.') }} €</p>
@endisland
PHP
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
    return [
        'orders'  => Order::whereMonth('created_at', now()->month)->count(),
        'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
    ];
}
In der Vorlage heißt eine berechnete Eigenschaft $this->products, niemals $products.

15. Zustand in URL und Session

PHP
use Livewire\Attributes\Url;
use Livewire\Attributes\Session;

new class extends Component {
    #[Url]
    public string $search = '';

    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    #[Url(except: '')]
    public string $sort = 'popular';

    #[Url(keep: true)]
    public int $perPage = 24;

    #[Session]
    public bool $sidebarCollapsed = false;
};
PHP
public function save(): void
{
    $this->validate();

    session()->flash('status', 'Einstellungen gespeichert');
}
Jede #[Url]-Eigenschaft ruft bei Änderung die History-API auf. Bei Feldern, die sich pro Tastendruck ändern, kombinieren Sie das mit wire:model.live.debounce.

16. Datei-Uploads

resources/views/components/⚡avatar-uploader.blade.php
<?php

use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;

new class extends Component {
    use WithFileUploads;

    #[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]
    public $avatar;

    public function save(): void
    {
        $this->validate();

        $path = $this->avatar->store('avatars', 'public');

        auth()->user()->update(['avatar_path' => $path]);

        $this->reset('avatar');
    }
};

?>

<div>
    <form wire:submit="save">
        <input type="file" wire:model="avatar" accept="image/*">

        <div wire:loading wire:target="avatar">Wird hochgeladen…</div>

        @if ($avatar)
            <img src="{{ $avatar->temporaryUrl() }}" alt="Vorschau" class="preview">
        @endif

        @error('avatar') <p class="error">{{ $message }}</p> @enderror

        <button type="submit" wire:loading.attr="disabled">Speichern</button>
    </form>
</div>
Blade
<div x-data="{ progress: 0, uploading: false }"
     x-on:livewire-upload-start="uploading = true"
     x-on:livewire-upload-finish="uploading = false; progress = 0"
     x-on:livewire-upload-progress="progress = $event.detail.progress">

    <input type="file" wire:model="avatar">

    <div x-show="uploading" class="progress">
        <div class="progress__bar" :style="`width: ${progress}%`"></div>
    </div>
</div>
upload_max_filesize und post_max_size in PHP sowie client_max_body_size in nginx müssen Ihre max:-Regel übersteigen.

17. Paginierung

PHP
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function with(): array
    {
        return [
            'orders' => Order::query()
                ->with('customer')
                ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
                ->paginate(25),
        ];
    }
};

Islands machen aus Paginierung eine Endlosliste

Blade
<div>
    <input type="search" wire:model.live.debounce.400ms="search">

    @island(name: 'rows')
        @foreach ($orders as $order)
            <div wire:key="order-{{ $order->id }}">{{ $order->number }}</div>
        @endforeach
    @endisland

    <button wire:click="nextPage" wire:island.append="rows">Mehr laden</button>
</div>
Bei großen Tabellen kostet paginate() ein zusätzliches COUNT(*). simplePaginate() ist spürbar günstiger.

18. Ladezustände

Blade
<div wire:loading>Wird geladen…</div>
<div wire:loading.remove>Inhalt</div>

<button wire:click="save">Speichern</button>
<span wire:loading wire:target="save">Wird gespeichert…</span>

<button wire:click="save" wire:loading.attr="disabled">Speichern</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Speichern</button>

<div wire:loading.delay>Wird geladen…</div>
<div wire:loading.delay.long>Wird geladen…</div>

Styling aus CSS

CSS
[data-loading] .btn {
    opacity: 0.5;
    pointer-events: none;
}
Blade
<input wire:model="title">

<span wire:dirty wire:target="title">Es gibt ungespeicherte Änderungen</span>

<div wire:offline class="banner banner--warn">
    Keine Verbindung zum Server.
</div>

19. Optimistisches UI auf dem Client

Eine ganze Klasse von Interaktionen brauchte den Server nie: ein Panel aufklappen, einen Zeichenzähler anzeigen, ein Feld rot färben. Version 4 gibt ihnen vollwertige Direktiven, die das DOM sofort ändern — ganz ohne Anfrage.

Blade
{{-- Schaltet die Sichtbarkeit per CSS, sofort --}}
<div wire:show="showModal" class="modal">…</div>

<button wire:click="$toggle('showModal')">Öffnen</button>
Blade
{{-- Der Text folgt der Eigenschaft auf dem Client --}}
<span wire:text="title"></span>

<input wire:model="title">
Blade
<textarea wire:model="message" maxlength="280"></textarea>

<span wire:text="message.length"></span> / 280

<span wire:bind:class="message.length > 240 && 'text-red-500'">
    Wird lang
</span>
Optimistisch heißt nicht maßgeblich. Diese Direktiven ändern, was der Nutzer sieht, bevor der Server zugestimmt hat. Alles, was Daten, Berechtigungen oder Geld betrifft, muss trotzdem serverseitig validiert und angewendet werden.

20. Drag and Drop

Blade
<ul wire:sort="reorder">
    @foreach ($tasks as $task)
        <li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            {{ $task->title }}
        </li>
    @endforeach
</ul>
PHP
new class extends Component {
    public function reorder(array $order): void
    {
        foreach ($order as $position => $id) {
            Task::where('id', $id)->update(['position' => $position]);
        }
    }
};

Anfasser

Blade
<ul wire:sort="reorder">
    @foreach ($tasks as $task)
        <li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            <span wire:sort:handle class="cursor-grab">⠿</span>

            <span>{{ $task->title }}</span>

            <button wire:sort:ignore wire:click="delete({{ $task->id }})">Löschen</button>
        </li>
    @endforeach
</ul>

Zwischen Listen ziehen

Blade
<div class="board">
    @foreach ($columns as $column)
        <ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
            @foreach ($column->cards as $card)
                <li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
                    {{ $card->title }}
                </li>
            @endforeach
        </ul>
    @endforeach
</div>
Die Reihenfolge muss persistiert werden. Die Direktive sortiert das DOM optimistisch um und übergibt Ihnen die neue Reihenfolge — sie in die Datenbank zu schreiben ist Ihre Aufgabe.

21. Scoped CSS und Skripte

resources/views/components/⚡pricing-card.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $plan = 'starter';
};

?>

<div class="card">
    <h3 class="title">{{ ucfirst($plan) }}</h3>
    <p class="price">{{ $this->price }} € / Monat</p>
</div>

<style>
    /* Auf diese Komponente begrenzt — keine globale Kollision bei .title */
    .card  { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
    .title { font-size: 20px; font-weight: 700; }
    .price { color: #2d6a4f; }
</style>
Blade
<style global>
    :root { --brand: #f53004; }
</style>
Blade
@script
<script>
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Scoped CSS ist eine Bequemlichkeit für komponentenlokale Gestaltung, kein Ersatz für ein Designsystem.

23. JavaScript-Integration

Blade
<div x-data="{ open: false }">
    <button x-on:click="open = !open">Details</button>

    <div x-show="open">
        <p x-text="$wire.title"></p>

        <button x-on:click="$wire.save()">Speichern</button>
        <button x-on:click="await $wire.calculate(); open = false">Berechnen</button>
    </div>
</div>

wire:ref

Blade
<livewire:modal wire:ref="modal" />

<button x-on:click="$refs.modal.open()">Modal öffnen</button>

#[Json]

PHP
use Livewire\Attributes\Json;

new class extends Component {
    #[Json]
    public function searchSuggestions(string $term): array
    {
        return Product::search($term)->take(5)->pluck('name')->all();
    }
};

Interceptors

JavaScript
document.addEventListener('livewire:init', () => {

    Livewire.interceptRequest(({ options, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();
            }
        });
    });

    Livewire.interceptMessage(({ component, succeed }) => {
        succeed(() => {
            console.debug('Komponente aktualisiert', component.name);
        });
    });
});
Blade
<div wire:ignore>
    <select id="select2-field">…</select>
</div>

24. Testing

Pest ist der empfohlene Weg, Livewire-4-Komponenten zu testen.

Bash
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.php
PHP
use Livewire\Livewire;

it('rendert', function () {
    Livewire::test('post.create')
        ->assertStatus(200);
});

it('validiert Pflichtfelder', function () {
    Livewire::test('post.create')
        ->set('title', '')
        ->call('save')
        ->assertHasErrors(['title' => 'required']);
});

it('speichert einen gültigen Beitrag', function () {
    Livewire::actingAs($user)
        ->test('post.create')
        ->set('title', 'Ein durchaus vernünftiger Titel')
        ->set('body', str_repeat('Ausreichend langer Beitragstext. ', 3))
        ->call('save')
        ->assertHasNoErrors()
        ->assertDispatched('notify');
});
MethodePrüft
assertSet() / assertNotSet()Eigenschaftswerte
assertSee() / assertDontSee()Ausgabe
assertHasErrors() / assertHasNoErrors()Validierung
assertDispatched()Events
assertRedirect()Weiterleitungen
assertForbidden()Autorisierung
Testen Sie zuerst Autorisierung in Aktionen, Validierungsregeln und Zustandsübergänge.

25. Sicherheit und Performance

Das Bedrohungsmodell ist identisch mit v3. Jede öffentliche Methode ist ein offener HTTP-Endpunkt, jede öffentliche Eigenschaft vom Client beschreibbar.

Sicherheits-Checkliste

  • $this->authorize() in jeder Aktion, die Daten verändert.
  • Alle Identifier mit #[Locked] markiert.
  • Hilfsmethoden als protected oder private deklariert.
  • Validierungsregeln ohne interne Felder (user_id, role, price).
  • Keine Tokens oder Schlüssel in öffentlichen Eigenschaften — der Snapshot geht im Klartext in den Browser.
  • Uploads nach MIME-Typ und Größe begrenzt.
  • Login-Formulare hinter einem Rate Limiter.
  • Optimistisches UI gilt nie als maßgeblich.

Performance

SymptomLösung in v4
Ein Widget-Update stößt alle Abfragen der Seite anBereiche in @island einpacken
Riesiges Seiten-MarkupModel-Collections aus öffentlichen Eigenschaften herausnehmen
Eine Anfrage pro Tastendruckwire:model.live.debounce.400ms
Last durch PollingPolling im Island plus .visible
Round Trip nur zum Umschalten eines Panelswire:show statt einer Aktion
Zähler-Inkrement rendert die Seite neu#[Renderless]
N+1-AbfragenEager Loading über with()
app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

26. Häufige Fehler und Spickzettel

Die Komponente rendert überhaupt nicht

Der Tag ist nicht selbstschließend. Schreiben Sie <livewire:my-component />.

In der Produktion alles 404, lokal läuft es

Der Livewire-Endpunkt ist von /livewire/ nach /livewire-{hash}/ gewandert. Eine Firewall-Regel, WAF-Ausnahme oder nginx-Location blockiert die Update-Anfragen.

Ein Feld synchronisiert nach dem Upgrade nicht mehr

Ergänzen Sie .live: wire:model.live.blur="title".

Ein Wrapper greift sein Feld nicht mehr ab

Verwenden Sie wire:model.deep.

wire:transition-Modifikatoren tun nichts

Sie wurden entfernt — v4 nutzt die native View Transitions API. Die Animation gehört ins CSS.

Ein Island aktualisiert nie

  • Das Island hat keinen name, die Aktion adressiert aber einen.
  • wire:island="…" zeigt auf einen Namen, den es in der Ausgabe nicht gibt.
  • Der Bereich hängt an Elternzustand, der sich gar nicht geändert hat — always: true ergänzen.

Volt-Komponenten funktionieren nicht mehr

Volt ist in den Kern gewandert. Ersetzen Sie Livewire\Volt\Component durch Livewire\Component, Volt::route() durch Route::livewire() und entfernen Sie Service Provider und Paket.

Direktiven-Spickzettel

DirektiveZweckNeu in v4
wire:modelFeld an Eigenschaft bindenSemantik geändert
wire:model.deepEvents von Kindelementen abfangenja
wire:click.asyncAktion parallel ausführenja
wire:click.renderlessNeurendern überspringenja
wire:islandEin Island adressierenja
wire:island.appendAn ein Island anhängenja
wire:showSichtbarkeit auf dem Client schaltenja
wire:textText clientseitig bindenja
wire:bindAttribut reaktiv bindenja
wire:sortDrag and Dropja
wire:intersectAktion beim Eintritt in den Viewportja
wire:refElement für JS benennenja
wire:navigate:scrollContainer-Scroll bewahrenumbenannt
wire:pollPeriodische Aktualisierungisland-begrenzt
wire:keyElement-Identität in SchleifenSmart Keys standardmäßig

Blade-Direktiven

DirektiveZweck
@island … @endislandUnabhängig rendernder Bereich
@placeholder … @endplaceholderPlatzhalter für ein Lazy-Island
<wire:slot name="…">Inhalt eines benannten Slots
@script … @endscriptKomponenten-eigenes JavaScript
@assets … @endassetsSeiten-Assets, einmalig geladen
<style> / <style global>Scoped / globales CSS

PHP-Attribute

AttributZweck
#[Validate] / #[Locked]Validierung und Schreibschutz
#[Computed]Gecachter abgeleiteter Wert
#[Url] / #[Session]Zustand erhalten
#[On] / #[Prop]Events und Props
#[Async] / #[Renderless]Parallelität und Render-Verzicht
#[Json]Daten direkt an JavaScript
Bash
php artisan make:livewire post.create              # Single-File
php artisan make:livewire post.create --mfc        # Multi-File
php artisan make:livewire post.create --test       # mit Test
php artisan livewire:publish --config
php artisan optimize:clear

Offizielle Quellen

Livewire 4 entwickelt sich schnell weiter. Wo dieser Leitfaden und die offizielle Dokumentation auseinandergehen, gilt die offizielle Quelle.