Livewire 3 — vollständiger Leitfaden

Mit Livewire bauen Sie dynamische Oberflächen in Laravel mit PHP und Blade — ohne separate Frontend-Anwendung und ohne API-Schicht. Dieser Leitfaden führt von der Installation bis in die Produktion: Komponenten, Zustand, Formulare, Events, Datei-Uploads, Testing, Sicherheit und Performance.

Livewire 3.x Laravel 10 / 11 / 12 PHP 8.1+ 24 Kapitel

1. Was ist Livewire

Livewire ist ein vollwertiges Framework für Laravel, mit dem sich dynamische Oberflächen in PHP und Blade bauen lassen, ohne das Backend zu verlassen. Sie schreiben keine separate SPA, brauchen keine REST- oder GraphQL-Schicht und pflegen kein zweites Datenmodell in JavaScript.

Funktionsweise

Eine Livewire-Komponente besteht aus einer PHP-Klasse und einer zugehörigen Blade-Vorlage. Beim ersten Seitenaufruf wird die Komponente serverseitig als normales Markup gerendert. Danach fängt eine kleine JS-Laufzeitumgebung Benutzerinteraktionen ab (Klick, Eingabe, Submit), schickt eine AJAX-Anfrage mit dem aktuellen Komponentenzustand an den Server, der Server rendert die Komponente neu und liefert frisches Markup zurück. Die Laufzeit vergleicht alten und neuen DOM-Baum und patcht gezielt nur die Unterschiede — die Seite lädt nicht neu.

Ablauf eines Zyklus
Browser                          Server
   │                                │
   │  Klick auf wire:click="save"   │
   ├───────── POST /livewire/update ┤
   │  { snapshot, calls, updates }  │
   │                                ├── Komponente aus Snapshot herstellen
   │                                ├── Eigenschaftsänderungen anwenden
   │                                ├── Methode save() aufrufen
   │                                ├── render() ausführen
   │  { snapshot, html, effects }   │
   ├◄───────────────────────────────┤
   ├── DOM morphen (nur Diffs)      │
   ▼                                ▼

Wann Livewire die richtige Wahl ist

  • Adminbereiche, CRM, Dashboards, interne Panels — viele Formulare und Tabellen, wenig aufwendige Animation.
  • Formulare mit abhängigen Feldern, mehrstufige Assistenten, Live-Validierung.
  • Tabellen mit Suche, Filtern, Sortierung und Paginierung.
  • Das Team ist stark in PHP und will keinen eigenen Frontend-Stack pflegen.
  • Features sollen schnell live gehen, ohne Logik zwischen PHP und JS zu duplizieren.

Wann etwas anderes besser passt

  • Oberflächen mit sehr hoher Aktualisierungsfrequenz: Online-Editoren, Canvas, Drag-and-drop-Builder, Spiele.
  • Offline-first-Anwendungen und PWAs mit komplexer lokaler Synchronisation.
  • Mobile Apps — dort brauchen Sie eine API, kein Markup über die Leitung.
  • Schlechte oder langsame Verbindungen: jeder Zyklus ist ein Netzwerk-Roundtrip.
Faustregel: Braucht eine Interaktion Daten vom Server, gehört sie zu Livewire. Ist die Interaktion rein visuell (Menü öffnen, Tab wechseln, Tooltip zeigen), gehört sie zu Alpine.js auf dem Client — ganz ohne Serveraufruf.

Versionen

Dieser Leitfaden beschreibt Livewire 3.x. Voraussetzungen: PHP 8.1+, Laravel 10 oder neuer. In Version 3 ist Alpine.js bereits enthalten, die Event-Namen haben sich geändert (dispatch statt emit), es gibt PHP-Attribute (#[Computed], #[Validate], #[Url]) und wire:model ist standardmäßig verzögert.

2. Installation und Einrichtung

Die Installation in ein bestehendes Laravel-Projekt ist ein einziger Befehl.

Bash
composer require livewire/livewire

In Livewire 3 müssen die Assets nicht separat eingebunden werden: das Paket fügt @livewireStyles und @livewireScripts selbst in das Layout ein. Bei einem abweichenden Layout oder aktiver CSP können Sie die Direktiven auch manuell setzen.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? 'Anwendung' }}</title>

    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    @livewireStyles
</head>
<body class="antialiased">

    {{ $slot }}

    @livewireScripts
</body>
</html>
Häufiger Fehler. Wenn die Komponente rendert, aber nicht auf Klicks reagiert, fehlt fast immer @livewireScripts im Layout — oder es steht vor Ihrem eigenen Alpine.js. Livewire 3 bringt Alpine bereits mit; eine zweite Alpine-Instanz zerstört die Reaktivität.

Konfiguration veröffentlichen

Bash
php artisan livewire:publish --config

Die wichtigsten Parameter in config/livewire.php:

ParameterZweck
class_namespaceNamespace der Komponentenklassen. Standard: App\Livewire.
view_pathVerzeichnis der Blade-Vorlagen.
layoutLayout für Full-Page-Komponenten.
temporary_file_uploadDisk, Lebensdauer und Regeln für temporäre Uploads.
inject_assetsAutomatisches Einfügen von CSS/JS. Abschalten, wenn Sie die Direktiven selbst setzen.
navigate.show_progress_barFortschrittsbalken für wire:navigate.

Installation prüfen

Bash
php artisan livewire:make Counter
# CLASS: app/Livewire/Counter.php
# VIEW:  resources/views/livewire/counter.blade.php

3. Die erste Komponente

Eine Komponente besteht aus zwei Dateien: Klasse und Vorlage.

app/Livewire/Counter.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 reset(): void
    {
        $this->count = 0;
    }

    public function render()
    {
        return view('livewire.counter');
    }
}
resources/views/livewire/counter.blade.php
<div class="flex items-center gap-3">
    <button type="button" wire:click="decrement" class="btn">−</button>

    <span class="text-2xl font-bold">{{ $count }}</span>

    <button type="button" wire:click="increment" class="btn">+</button>

    <button type="button" wire:click="reset" class="btn btn-ghost">Zurücksetzen</button>
</div>
Genau ein Wurzelelement. Die Vorlage einer Komponente muss exakt ein Wurzelelement besitzen. Auch Kommentare und Text auf oberster Ebene brechen den Morph-Algorithmus. Brauchen Sie mehrere Blöcke, verpacken Sie sie in ein <div>.

Drei Wege, eine Komponente einzubinden

Blade
{{-- 1. Tag-Syntax (empfohlen) --}}
<livewire:counter />

{{-- 2. Direktive --}}
@livewire('counter')

{{-- 3. Mit Parametern --}}
<livewire:counter :start="10" title="Bestellzähler" />

Full-Page-Komponenten

Eine Komponente lässt sich direkt an eine Route hängen und rendert dann als eigenständige Seite.

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

Route::get('/dashboard', Dashboard::class)->middleware('auth')->name('dashboard');
app/Livewire/Dashboard.php
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;

class Dashboard extends Component
{
    #[Layout('layouts.app')]
    #[Title('Übersicht')]
    public function render()
    {
        return view('livewire.dashboard');
    }
}

Inline-Komponenten

Bei sehr kleinen Komponenten können Sie die Vorlage als String zurückgeben — eine eigene Blade-Datei entfällt.

PHP
public function render()
{
    return <<<'BLADE'
        <div>
            <button wire:click="$refresh">Aktualisieren</button>
        </div>
    BLADE;
}

4. Eigenschaften der Komponente

Öffentliche Eigenschaften der Klasse sind automatisch in der Blade-Vorlage verfügbar und bleiben zwischen Anfragen erhalten. Sie bilden den Zustand der Komponente.

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

    public function render()
    {
        return view('livewire.user-profile');
    }
}

Welche Typen erlaubt sind

Zwischen den Anfragen wird der Zustand als JSON serialisiert, deshalb ist die Menge zulässiger Typen begrenzt.

ErlaubtNicht erlaubt
string, int, float, bool, null Closures
array aus skalaren Werten Ressourcen (resource), Streams
Eloquent-Modelle und Collection von Modellen Beliebige Objekte ohne Wireable/Synth-Unterstützung
Carbon, DateTime, Stringable, Enums Objekte mit Verbindungs- oder PDO-Referenzen
Eloquent-Modelle als Eigenschaften. Ein Modell wird als Identifier gespeichert und bei jeder Anfrage neu aus der Datenbank geladen. Das ist bequem, kostet aber pro Zyklus eine SQL-Abfrage. Wenn Sie nur wenige Felder brauchen, speichern Sie Skalare statt des ganzen Modells.

Initialisierung: mount()

mount() ist der Konstruktor der Komponente. Die Methode läuft einmal beim ersten Rendern und erhält die aus Blade übergebenen Parameter.

PHP
use App\Models\Order;

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

    public function mount(Order $order, string $mode = 'compact'): void
    {
        $this->order = $order;
        $this->mode  = $mode;
    }
}
Blade
<livewire:order-card :order="$order" mode="full" :key="$order->id" />

Eigenschaften schützen: #[Locked]

Standardmäßig kann der Client den Wert jeder öffentlichen Eigenschaft ändern. Bei Identifiern und allem, was Zugriffsrechte beeinflusst, ist das eine Sicherheitslücke. Das Attribut #[Locked] verbietet Änderungen vom Frontend.

PHP
use Livewire\Attributes\Locked;

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

    public string $comment = '';
}

Vor JSON verborgene Eigenschaften

Eigenschaften mit protected oder private werden nicht serialisiert und zwischen Anfragen zurückgesetzt. Nutzen Sie sie nur für Werte, die in jedem Zyklus neu berechnet werden.

PHP
class Report extends Component
{
    public string $period = 'month';

    // Wird nach jeder Anfrage geleert — hier keinen Zustand ablegen.
    protected array $cache = [];
}

5. wire:model und Datenbindung

wire:model bindet ein Formularfeld an eine Eigenschaft der Komponente. In Livewire 3 ist die Bindung standardmäßig verzögert: der Wert geht nicht bei jedem Tastendruck an den Server, sondern zusammen mit der nächsten Aktion (Klick, Submit).

Blade
{{-- Verzögert: der Wert geht mit der nächsten Anfrage --}}
<input type="text" wire:model="name">

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

{{-- Mit 500 ms Verzögerung nach der Eingabe --}}
<input type="text" wire:model.live.debounce.500ms="search">

{{-- Höchstens alle 2 s, während getippt wird --}}
<input type="text" wire:model.live.throttle.2s="search">

{{-- Erst beim Verlassen des Feldes --}}
<input type="text" wire:model.blur="email">

{{-- Wert ändert sich sofort im Client, der Server erfährt es später --}}
<input type="text" wire:model.lazy="draft">

Modifikatoren

ModifikatorVerhaltenEinsatz
Verzögertes SendenNormale Formularfelder
.liveAnfrage bei jeder ÄnderungLive-Suche, abhängige Selects
.blurAnfrage beim FokusverlustFeldvalidierung beim Verlassen
.debounce.XmsAuf Eingabepause wartenSuche während des Tippens
.throttle.XsHöchstens einmal pro IntervallTeure Abfragen
.numberIn Zahl umwandelnNumerische Felder
.booleanIn Boolean umwandelnJa/Nein-Selects
.fillStartwert aus dem Markup übernehmenVorausgefüllte Formulare

Alle Feldtypen

Blade
{{-- Text, Textarea --}}
<input type="text" wire:model="title">
<textarea wire:model="body"></textarea>

{{-- Einzelne Checkbox: bool --}}
<input type="checkbox" wire:model="agreed">

{{-- Checkbox-Gruppe: array --}}
<input type="checkbox" value="php"  wire:model="skills">
<input type="checkbox" value="js"   wire:model="skills">
<input type="checkbox" value="sql"  wire:model="skills">

{{-- Radiobuttons --}}
<input type="radio" value="card" wire:model="payment">
<input type="radio" value="cash" wire:model="payment">

{{-- Select --}}
<select wire:model.live="categoryId">
    <option value="">Alle Kategorien</option>
    @foreach ($categories as $category)
        <option value="{{ $category->id }}">{{ $category->name }}</option>
    @endforeach
</select>

{{-- Mehrfachauswahl --}}
<select wire:model="tagIds" multiple>
    @foreach ($tags as $tag)
        <option value="{{ $tag->id }}">{{ $tag->name }}</option>
    @endforeach
</select>

Verschachtelte Daten

Die Punktnotation funktioniert für Arrays und für Modelleigenschaften.

PHP
public array $form = [
    'name'    => '',
    'address' => ['city' => '', 'street' => ''],
];

public Post $post;
Blade
<input wire:model="form.name">
<input wire:model="form.address.city">
<input wire:model="post.title">
Bindung an ein Modell. Damit wire:model="post.title" funktioniert, muss das Feld durch eine Validierungsregel freigegeben sein (rules oder #[Validate]) — sonst wirft Livewire eine Exception. Das schützt vor Mass Assignment.

6. Aktionen (Actions)

Eine Aktion ist eine öffentliche Methode der Komponente, die aus der Vorlage aufgerufen wird. Sie ersetzt die übliche Kombination aus fetch() und serverseitigem Handler.

Blade
{{-- Klick --}}
<button wire:click="save">Speichern</button>

{{-- Formular absenden (submit wird abgefangen) --}}
<form wire:submit="save">
    <input wire:model="title">
    <button type="submit">Absenden</button>
</form>

{{-- Tasten --}}
<input wire:keydown.enter="search" wire:keydown.escape="clear">

{{-- Weitere DOM-Events --}}
<div wire:mouseenter="preload">…</div>
<select wire:change="applyFilter">…</select>

Parameter

Blade
<button wire:click="delete({{ $post->id }})">Löschen</button>
<button wire:click="setStatus('published')">Veröffentlichen</button>
<button wire:click="move({{ $item->id }}, 'up')">Nach oben</button>
PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);   // Autorisierung immer prüfen

    $post->delete();

    $this->dispatch('notify', message: 'Beitrag gelöscht');
}
Parametern niemals vertrauen. Jede Methode der Komponente ist als öffentlicher HTTP-Endpunkt erreichbar. Ein Client kann delete(999) mit beliebiger id aufrufen. Die Rechteprüfung innerhalb der Methode ist Pflicht.

Model Binding in Parametern

Livewire löst Modelle anhand der id auf, genau wie der Router in Laravel.

PHP
public function archive(Post $post): void
{
    $this->authorize('update', $post);

    $post->update(['archived_at' => now()]);
}

Modifikatoren für Aktionen

Blade
{{-- Bestätigung im Browser vor dem Absenden --}}
<button wire:click="delete" wire:confirm="Wirklich löschen? Das lässt sich nicht rückgängig machen.">Löschen</button>

{{-- preventDefault / stopPropagation --}}
<a href="#" wire:click.prevent="open">Öffnen</a>
<div wire:click.stop="select">…</div>

{{-- Läuft genau einmal --}}
<button wire:click.once="init">Initialisieren</button>

{{-- Nur bei Klick auf genau dieses Element --}}
<div wire:click.self="close">…</div>

Magische Aktionen

AktionWirkung
$refreshKomponente neu rendern, ohne den Zustand zu ändern
$set('prop', value)Eigenschaft einen Wert zuweisen
$toggle('prop')Boolean-Eigenschaft umschalten
$dispatch('event')Livewire-Event senden
$parent.method()Methode der Elternkomponente aufrufen
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>

Weiterleitungen

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

    $post = Post::create($this->only('title', 'body'));

    session()->flash('status', 'Beitrag erstellt');

    $this->redirect(route('posts.show', $post), navigate: true);
}

7. Lebenszyklus einer Anfrage

Wer die Reihenfolge kennt, spart sich 90 % der „mysteriösen“ Fehler. Zu unterscheiden sind das erste Rendern (normale HTTP-Anfrage der Seite) und die Folgeaktualisierungen (AJAX-Anfragen von Livewire).

Erstes Rendern

Reihenfolge
1. Instanz der Komponente wird erzeugt
2. boot()
3. mount($params)
4. booted()
5. Hydrate-Hooks werden NICHT aufgerufen
6. render()
7. Markup wird in die Seite eingefügt

Folgeaktualisierung

Reihenfolge
 1. POST /livewire/update mit dem Zustands-Snapshot trifft ein
 2. Prüfsumme des Snapshots wird verifiziert (Manipulationsschutz)
 3. Instanz der Komponente wird erzeugt
 4. boot()
 5. Zustand wird aus dem Snapshot wiederhergestellt
 6. hydrate() und hydrateFoo() für jede Eigenschaft
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)   — vor der Zuweisung
 9. Eigenschaften erhalten die neuen Werte
10. updated($prop, $value) / updatedFoo($value)     — nach der Zuweisung
11. Methoden aus der calls-Queue werden ausgeführt (Aktionen)
12. rendering()
13. render()
14. rendered($view, $html)
15. dehydrate() — Zustand wird zurück in den Snapshot gepackt
16. Antwort: neues Markup plus effects (Events, Redirects, Dispatch an den Browser)

Snapshot und Prüfsumme

Der gesamte Zustand reist im Attribut wire:snapshot zum Client und zurück. Damit der Client die Daten nicht manipuliert, signiert Livewire den Snapshot per HMAC auf Basis des APP_KEY. Stimmt die Signatur nicht, wird die Anfrage mit „Livewire encountered corrupt data“ abgelehnt.

Praktische Folge. Je mehr Zustand in öffentlichen Eigenschaften liegt, desto mehr Daten wandern pro Zyklus durchs Netz. Eine Collection mit 500 Modellen in einer öffentlichen Eigenschaft bedeutet Megabytes pro Klick. Halten Sie den Zustand klein und holen Sie Listen in render() oder über #[Computed].

Was mit dem DOM passiert

Livewire ersetzt keinen kompletten Knoten, sondern führt ein Morph aus: alter und neuer Baum werden verglichen und nur die Unterschiede angepasst. Deshalb bleiben Eingabefokus, Scrollposition und Alpine-Zustand erhalten. Ändert sich die Struktur einer Liste, braucht der Algorithmus einen Hinweis — siehe wire:key in Kapitel 10.

8. Lifecycle-Hooks

Hooks erlauben den Eingriff in jede Phase des Zyklus.

PHP
class ProductEditor extends Component
{
    public Product $product;
    public string $name = '';
    public float $price = 0;

    // Läuft zu Beginn JEDER Anfrage, vor der Wiederherstellung des Zustands.
    public function boot(): void
    {
        // Guter Ort für Abhängigkeiten, die sich nicht serialisieren lassen.
    }

    // Nur beim ersten Rendern.
    public function mount(Product $product): void
    {
        $this->product = $product;
        $this->name    = $product->name;
        $this->price   = $product->price;
    }

    // Nach der Wiederherstellung des Zustands, in jeder Folgeanfrage.
    public function hydrate(): void
    {
    }

    // Nach boot() und Wiederherstellung des Zustands.
    public function booted(): void
    {
    }

    // Vor der Änderung einer beliebigen Eigenschaft.
    public function updating(string $property, mixed $value): void
    {
    }

    // Nach der Änderung einer beliebigen Eigenschaft.
    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    // Nur für die Eigenschaft $price.
    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }

    // Für den verschachtelten Schlüssel form.email
    public function updatedFormEmail(mixed $value): void
    {
        $this->form['email'] = strtolower(trim($value));
    }

    public function rendering(): void
    {
    }

    public function rendered(mixed $view, string $html): void
    {
    }

    // Vor dem Verpacken des Zustands in den Snapshot.
    public function dehydrate(): void
    {
    }

    public function render()
    {
        return view('livewire.product-editor');
    }
}

Namensregeln

EigenschaftHook
$priceupdatedPrice()
$isActiveupdatedIsActive()
$form['email']updatedFormEmail()
$post->titleupdatedPostTitle()
Typisches Muster. Das Zurücksetzen der Paginierung bei Filteränderung gehört genau in einen Hook: public function updatedSearch() { $this->resetPage(); }

9. Validierung und Form-Objekte

Livewire nutzt den Validator von Laravel — Regeln, Meldungen und Lokalisierung sind identisch.

Variante 1: Attribute (Livewire 3)

PHP
use Livewire\Attributes\Validate;

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

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

    #[Validate('required|string|min:20|max:2000')]
    public string $message = '';

    #[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();

        session()->flash('status', 'Nachricht gesendet');
    }
}

Variante 2: Methode rules()

Nötig, wenn die Regeln vom Zustand abhängen.

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',
    ];
}

protected function messages(): array
{
    return [
        'email.unique' => 'Diese E-Mail-Adresse ist bereits registriert.',
    ];
}

protected function validationAttributes(): array
{
    return [
        'email' => 'E-Mail-Adresse',
    ];
}

Live-Validierung

PHP
public function updated(string $property): void
{
    $this->validateOnly($property);   // nur das geänderte Feld prüfen
}
Blade
<form wire:submit="submit" novalidate>
    <label for="email">E-Mail</label>
    <input id="email" type="email" wire:model.blur="email"
           class="@error('email') border-red-500 @enderror">

    @error('email')
        <p class="text-sm text-red-600">{{ $message }}</p>
    @enderror

    <button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove wire:target="submit">Absenden</span>
        <span wire:loading wire:target="submit">Wird gesendet…</span>
    </button>
</form>

Fehler manuell steuern

PHP
$this->addError('email', 'Die Domain steht auf der Sperrliste.');
$this->resetValidation('email');
$this->resetValidation();               // alle zurücksetzen
$this->validateOnly('email');

// Fehler werfen wie in einem gewöhnlichen Laravel-Controller
throw ValidationException::withMessages([
    'code' => 'Falscher Bestätigungscode.',
]);

Form-Objekte

Wächst ein Formular, lagern Sie es in eine eigene Klasse aus. Die Komponente bleibt schlank, und Regeln wie Daten lassen sich zwischen Anlegen und Bearbeiten wiederverwenden.

Bash
php artisan livewire:form PostForm
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 = '';

    #[Validate('nullable|date|after_or_equal:today')]
    public ?string $publishAt = null;

    public function setPost(Post $post): void
    {
        $this->post      = $post;
        $this->title     = $post->title;
        $this->body      = $post->body;
        $this->publishAt = $post->publish_at?->toDateString();
    }

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

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

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

        $this->post->update($this->except('post'));
    }
}
app/Livewire/PostEditor.php
class PostEditor extends Component
{
    public PostForm $form;

    public function mount(?Post $post = null): void
    {
        if ($post?->exists) {
            $this->form->setPost($post);
        }
    }

    public function save(): void
    {
        $this->form->post
            ? $this->form->update()
            : $this->form->store();

        $this->redirect(route('posts.index'), navigate: true);
    }
}
Blade
<form wire:submit="save">
    <input wire:model="form.title">
    @error('form.title') <span>{{ $message }}</span> @enderror

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

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

10. Verschachtelte Komponenten und wire:key

Komponenten lassen sich verschachteln. Jede Kindkomponente ist eine eigenständige Einheit mit eigenem Zustand und eigenem Aktualisierungszyklus: ein Klick im Kind rendert das Elternteil nicht neu.

Blade
<div>
    <h1>Bestellungen</h1>

    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>
:key ist in Schleifen Pflicht. Ohne eindeutigen Schlüssel verwechselt der Morph-Algorithmus die Elemente beim Sortieren, Filtern oder Löschen: Feldwerte „wandern“ in Nachbarzeilen. Der Schlüssel muss stabil sein — $loop->index taugt nicht, nehmen Sie die id des Datensatzes.

Daten nach unten reichen

Parameter werden einmalig in mount() übergeben. Bei weiteren Renderings des Elternteils aktualisiert sich das Kind nicht automatisch — es lebt sein eigenes Leben.

PHP
use Livewire\Attributes\Reactive;

class OrderTotal extends Component
{
    // Mit #[Reactive] kommt der Wert bei jedem Rendern des Elternteils erneut an.
    #[Reactive]
    public int $quantity;
}

Das Elternteil aufrufen

Blade
<button wire:click="$parent.refreshList()">Liste aktualisieren</button>

Zwei-Wege-Bindung: #[Modelable]

Erlaubt wire:model direkt auf der Komponente — praktisch für Eingabe-Widgets.

app/Livewire/RatingInput.php
use Livewire\Attributes\Modelable;

class RatingInput extends Component
{
    #[Modelable]
    public int $value = 0;

    public function set(int $value): void
    {
        $this->value = $value;
    }
}
Blade
{{-- In der Elternvorlage --}}
<livewire:rating-input wire:model.live="review.rating" />

Bedingtes Rendern von Kindkomponenten

Blade
@if ($showDetails)
    <livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif
Wann aufteilen. Ziehen Sie eine Kindkomponente heraus, wenn der Block eigenen Zustand hat oder sich unabhängig aktualisiert (Tabellenzeile, Modal, Widget). Ist der Block reines Markup, nehmen Sie ein normales Blade-Partial — das ist günstiger.

11. Events

Events verbinden Komponenten, die nicht in einer Eltern-Kind-Beziehung stehen. In Livewire 3 wird mit dispatch() gesendet (in Version 2 war es emit()).

Senden

PHP
// Einfaches Event
$this->dispatch('post-created');

// Mit benannten Parametern
$this->dispatch('post-created', postId: $post->id, title: $post->title);

// Nur an eine bestimmte Komponente
$this->dispatch('refresh')->to(OrderList::class);

// Nur an sich selbst
$this->dispatch('recalculate')->self();
Blade
{{-- Direkt aus der Vorlage --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">Neue Bestellung</button>

Empfangen

PHP
use Livewire\Attributes\On;

class OrderList extends Component
{
    public array $orders = [];

    #[On('post-created')]
    public function onPostCreated(int $postId, string $title): void
    {
        $this->orders[] = ['id' => $postId, 'title' => $title];
    }

    // Dynamischer Event-Name
    #[On('order-updated.{orderId}')]
    public function onOrderUpdated(): void
    {
        $this->refreshList();
    }
}

Alternative ist das Array $listeners (kompatibel zu Version 2):

PHP
protected $listeners = [
    'post-created' => 'onPostCreated',
    'refresh'      => '$refresh',
];

Events an den Browser

Livewire kann ein gewöhnliches DOM-Event senden — abgefangen von Alpine.js oder Ihrem eigenen JS. Das ist der richtige Weg für Toasts und für das Öffnen von Modals.

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>
JavaScript
document.addEventListener('notify', (event) => {
    console.log(event.detail.message);
});
Events sparsam einsetzen. Jedes Event bedeutet einen zusätzlichen Anfragezyklus für jede lauschende Komponente. Ändern sich zwei Blöcke immer gemeinsam, ist es oft günstiger, sie zu einer Komponente zusammenzufassen.

12. Berechnete Eigenschaften

#[Computed] liefert abgeleitete Daten, ohne sie im Zustand zu speichern. Das Ergebnis wird für die Dauer einer Anfrage gecacht — dreimaliger Zugriff in der Vorlage erzeugt also nicht drei SQL-Abfragen.

PHP
use Livewire\Attributes\Computed;
use Illuminate\Support\Collection;

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

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

    #[Computed]
    public function subtotal(): float
    {
        return $this->products->sum(
            fn (Product $product) => $product->price * $this->items[$product->id]
        );
    }

    #[Computed]
    public function total(): float
    {
        return round($this->subtotal * 1.19, 2);   // inkl. MwSt.
    }
}
Blade
<div>
    @foreach ($this->products as $product)
        <div>{{ $product->name }} — {{ $this->items[$product->id] }} Stk.</div>
    @endforeach

    <p>Zwischensumme: {{ number_format($this->subtotal, 2, ',', '.') }} €</p>
    <p>Gesamt inkl. MwSt.: {{ number_format($this->total, 2, ',', '.') }} €</p>
</div>
Zugriff über $this. In der Vorlage ist eine berechnete Eigenschaft als $this->products erreichbar, nicht als $products. Darin unterscheidet sie sich von gewöhnlichen öffentlichen Eigenschaften.

Caching über Anfragen hinweg

PHP
// Cache für 5 Minuten im gemeinsamen Anwendungscache
#[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'),
    ];
}

// Gemeinsamer Cache für alle Benutzer
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
    return app(StatsService::class)->build();
}

Cache verwerfen

PHP
public function addItem(int $productId): void
{
    $this->items[$productId] = ($this->items[$productId] ?? 0) + 1;

    unset($this->products, $this->subtotal, $this->total);   // Cache leeren
}

Daten in render() vs. #[Computed]

Beide Varianten speichern nichts im Zustand. Der Unterschied liegt in der Sichtbarkeit: render() übergibt Variablen nur an die Vorlage, während #[Computed] in PHP-Methoden und in der Vorlage verfügbar ist und zusätzlich gecacht wird.

PHP
public function render()
{
    return view('livewire.orders', [
        'orders' => Order::query()
            ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
            ->latest()
            ->paginate(20),
    ]);
}

13. Zustand in URL und Session

Filter, Suche und aktiver Tab sollten einen Reload überstehen und sich als Link teilen lassen. Das Attribut #[Url] synchronisiert eine Eigenschaft mit dem Query-String.

PHP
use Livewire\Attributes\Url;

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

    // Abweichender Parametername in der URL
    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    // Nicht in der URL anzeigen, solange der Wert dem Startwert entspricht
    #[Url(except: '')]
    public string $sort = 'popular';

    // Über wire:navigate-Wechsel hinweg behalten
    #[Url(keep: true)]
    public int $perPage = 24;

    // history.pushState statt replaceState verwenden
    #[Url(history: true)]
    public string $tab = 'all';
}

Ergebnis: /catalog?search=notebook&cat=5&sort=price-asc&tab=sale

Speichern in der Session

PHP
use Livewire\Attributes\Session;

class Sidebar extends Component
{
    #[Session]
    public bool $collapsed = false;

    #[Session(key: 'admin.table.density')]
    public string $density = 'comfortable';
}

Session und Flash-Meldungen manuell

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

    Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);

    session()->flash('status', 'Einstellungen gespeichert');
}
Blade
@if (session('status'))
    <div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif
Nicht alles in die URL packen. Jede #[Url]-Eigenschaft ruft bei Änderung die History-API auf. Bei Feldern, die sich mit jedem Tastendruck ändern, kombinieren Sie das mit wire:model.live.debounce.

14. Datei-Uploads

Das Trait WithFileUploads ergänzt vollwertige Uploads: die Datei geht direkt nach der Auswahl an den Server, landet im temporären Speicher und steht als Objekt TemporaryUploadedFile bereit.

PHP
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;

class AvatarUploader extends Component
{
    use WithFileUploads;

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

    #[Validate(['documents.*' => 'file|mimes:pdf,docx|max:10240'])]
    public array $documents = [];

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

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

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

        $this->reset('avatar');

        $this->dispatch('notify', message: 'Avatar aktualisiert');
    }

    public function removeDocument(int $index): void
    {
        unset($this->documents[$index]);

        $this->documents = array_values($this->documents);
    }
}
Blade
<form wire:submit="save">
    <input type="file" wire:model="avatar" accept="image/*">

    {{-- Upload-Fortschritt --}}
    <div wire:loading wire:target="avatar" class="text-sm">Datei wird hochgeladen…</div>

    {{-- Vorschau vor dem Speichern --}}
    @if ($avatar)
        <img src="{{ $avatar->temporaryUrl() }}" alt="Vorschau" class="w-32 h-32 object-cover rounded">
    @endif

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

    <button type="submit" wire:loading.attr="disabled">Speichern</button>
</form>

Genaue Fortschrittsanzeige

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-error="uploading = false"
     x-on:livewire-upload-progress="progress = $event.detail.progress">

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

    <div x-show="uploading" class="h-2 bg-gray-200 rounded">
        <div class="h-2 bg-green-600 rounded" :style="`width: ${progress}%`"></div>
    </div>
</div>

Temporären Speicher konfigurieren

config/livewire.php
'temporary_file_upload' => [
    'disk'       => 's3',            // oder null → Standard-Disk
    'rules'      => ['file', 'max:12288'],
    'directory'  => 'livewire-tmp',
    'middleware' => 'throttle:60,1',
    'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
    'max_upload_time' => 5,          // Minuten bis zur automatischen Bereinigung
],
PHP-Limits prüfen. upload_max_filesize, post_max_size und max_execution_time in der php.ini sowie client_max_body_size in nginx müssen mindestens so groß sein wie Ihr max: in den Regeln. Sonst bricht der Upload ohne verständliche Fehlermeldung ab.

15. Paginierung

Das Trait WithPagination bindet die Laravel-Paginierung ohne Seitenreload ein.

PHP
use Livewire\WithPagination;
use Livewire\Attributes\Url;

class OrderTable extends Component
{
    use WithPagination;

    #[Url]
    public string $search = '';

    #[Url]
    public string $status = '';

    public string $sortField = 'created_at';
    public string $sortDirection = 'desc';
    public int $perPage = 25;

    // Bei Filterwechsel zurück auf Seite eins.
    public function updatedSearch(): void
    {
        $this->resetPage();
    }

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

    public function sortBy(string $field): void
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortField = $field;
            $this->sortDirection = 'asc';
        }

        $this->resetPage();
    }

    public function render()
    {
        return view('livewire.order-table', [
            'orders' => Order::query()
                ->with('customer')
                ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
                ->when($this->status, fn ($q) => $q->where('status', $this->status))
                ->orderBy($this->sortField, $this->sortDirection)
                ->paginate($this->perPage),
        ]);
    }
}
Blade
<div>
    <input type="search" wire:model.live.debounce.400ms="search" placeholder="Nach Nummer suchen">

    <table>
        <thead>
            <tr>
                <th wire:click="sortBy('number')" style="cursor:pointer">Nummer</th>
                <th wire:click="sortBy('created_at')" style="cursor:pointer">Datum</th>
                <th wire:click="sortBy('total')" style="cursor:pointer">Betrag</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($orders as $order)
                <tr wire:key="order-{{ $order->id }}">
                    <td>{{ $order->number }}</td>
                    <td>{{ $order->created_at->format('d.m.Y') }}</td>
                    <td>{{ number_format($order->total, 2, ',', '.') }}</td>
                </tr>
            @empty
                <tr><td colspan="3">Keine Bestellungen gefunden</td></tr>
            @endforelse
        </tbody>
    </table>

    {{ $orders->links() }}
</div>

Nützliche Details

PHP
// Eigener Name für den Seitenparameter — nötig bei zwei Paginatoren auf einer Seite
protected string $paginationTheme = 'tailwind';   // oder 'bootstrap'

public function render()
{
    return view('livewire.dashboard', [
        'orders'   => Order::paginate(10, pageName: 'orders-page'),
        'invoices' => Invoice::paginate(10, pageName: 'invoices-page'),
    ]);
}
Performance. Bei großen Tabellen führt paginate() zusätzlich ein COUNT(*) aus. Wenn Sie die Gesamtzahl der Seiten nicht brauchen, nehmen Sie simplePaginate() — das ist deutlich günstiger.

16. Ladezustände

Jede Aktion ist eine Netzwerkanfrage. Ohne Rückmeldung wirkt die Oberfläche eingefroren. Livewire bietet deklarative Direktiven, die ohne eine Zeile JS auskommen.

Blade
{{-- Während jeder Anfrage der Komponente anzeigen --}}
<div wire:loading>Wird geladen…</div>

{{-- Während der Anfrage ausblenden --}}
<div wire:loading.remove>Inhalt</div>

{{-- Nur für eine bestimmte Aktion --}}
<button wire:click="save">Speichern</button>
<span wire:loading wire:target="save">Wird gespeichert…</span>

{{-- Für mehrere Ziele --}}
<span wire:loading wire:target="save,delete,publish">Wird verarbeitet…</span>

{{-- Für eine bestimmte Eigenschaft --}}
<span wire:loading wire:target="search">Wird gesucht…</span>

{{-- Ziel ausschließen --}}
<div wire:loading wire:target.except="search">Wird aktualisiert…</div>

Modifikatoren

Blade
{{-- Button sperren --}}
<button wire:click="save" wire:loading.attr="disabled">Speichern</button>

{{-- CSS-Klasse hinzufügen --}}
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Speichern</button>

{{-- Klasse entfernen --}}
<div wire:loading.class.remove="bg-white">…</div>

{{-- Verzögerung: Indikator nur, wenn die Anfrage länger als 300 ms dauert --}}
<div wire:loading.delay>Wird geladen…</div>

{{-- Feste Schwellen: shortest 50ms, shorter 100ms, short 150ms,
     default 200ms, long 300ms, longer 500ms, longest 1s --}}
<div wire:loading.delay.long>Wird geladen…</div>

{{-- Anzeigeart steuern --}}
<div wire:loading.flex>…</div>
<div wire:loading.grid>…</div>
<div wire:loading.inline-flex>…</div>

Ungespeicherte Änderungen: wire:dirty

Blade
<input wire:model="title">

<span wire:dirty wire:target="title" class="text-amber-600">Es gibt ungespeicherte Änderungen</span>

<button wire:click="save" wire:dirty.class="ring-2 ring-amber-400">Speichern</button>

Verbindungsverlust: wire:offline

Blade
<div wire:offline class="banner banner--warn">
    Keine Verbindung zum Server. Änderungen werden nicht gespeichert.
</div>

Skeleton beim ersten Laden

Blade
<div wire:init="loadHeavyData">
    @if ($loaded)
        {{-- echte Daten --}}
    @else
        <div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
    @endif
</div>

18. Lazy Loading und Polling

Komponente verzögert laden

Eine schwere Komponente muss nicht in der ersten Antwort gerendert werden: die Seite kommt sofort mit einem Platzhalter, der Inhalt folgt mit einer zweiten Anfrage.

Blade
<livewire:revenue-chart lazy />
PHP
use Livewire\Attributes\Lazy;

#[Lazy]
class RevenueChart extends Component
{
    public function placeholder(): string
    {
        return <<<'BLADE'
            <div class="skeleton h-64 w-full animate-pulse rounded bg-gray-200"></div>
        BLADE;
    }

    public function render()
    {
        return view('livewire.revenue-chart', [
            'points' => app(RevenueService::class)->monthly(),   // teure Abfrage
        ]);
    }
}

Standardmäßig lädt eine Lazy-Komponente direkt nach dem Seitenaufbau. Mit #[Lazy(isolate: false)] werden die Anfragen mehrerer Lazy-Komponenten gebündelt, und lazy="on-load" / lazy="on-scroll" steuern den Ladezeitpunkt.

Blade
{{-- Laden, sobald der Block im Viewport erscheint --}}
<livewire:revenue-chart lazy="on-scroll" />

Verzögerte Initialisierung: wire:init

Blade
<div wire:init="loadStats">
    @if ($stats)
        …
    @else
        <div class="skeleton"></div>
    @endif
</div>

Server abfragen: wire:poll

Blade
{{-- Alle 2 s aktualisieren (Standardwert sind 2500 ms) --}}
<div wire:poll>…</div>

{{-- Eigenes Intervall --}}
<div wire:poll.5s>…</div>
<div wire:poll.750ms>…</div>

{{-- Bestimmte Methode aufrufen --}}
<div wire:poll.10s="refreshQueue">…</div>

{{-- Abfrage stoppen, wenn der Tab inaktiv ist --}}
<div wire:poll.visible.5s="refreshQueue">…</div>

{{-- Nach 5 Minuten Benutzerinaktivität stoppen --}}
<div wire:poll.keep-alive.5s>…</div>
Was Polling kostet. wire:poll.2s auf einer Seite, die bei 100 Mitarbeitenden offen ist, sind 3000 Anfragen pro Minute an Ihren PHP-Prozess. Setzen Sie immer .visible, wählen Sie ein vernünftiges Intervall und ziehen Sie für wirklich Echtzeit-Daten WebSockets in Betracht (Laravel Echo + Reverb).

19. Alpine.js, $wire und JS-Hooks

Alpine.js ist Bestandteil von Livewire 3. Innerhalb einer Komponente steht das Objekt $wire zur Verfügung — ein Proxy auf Zustand und Methoden der PHP-Komponente, direkt aus JavaScript.

Blade
<div x-data="{ open: false }">
    {{-- Rein clientseitiger Zustand: der Server ist nicht beteiligt --}}
    <button x-on:click="open = !open">Details</button>

    <div x-show="open" x-transition>
        {{-- Eigenschaft lesen --}}
        <p x-text="$wire.title"></p>

        {{-- Eigenschaft schreiben --}}
        <button x-on:click="$wire.title = 'Neuer Titel'">Umbenennen</button>

        {{-- Methode aufrufen (gibt ein Promise zurück) --}}
        <button x-on:click="$wire.save()">Speichern</button>

        {{-- Methode mit Warten auf das Ergebnis --}}
        <button x-on:click="await $wire.calculate(); open = false">Berechnen</button>

        {{-- Aufruf ohne Neurendern --}}
        <button x-on:click="$wire.$set('tab', 'stats', false)">Statistik</button>
    </div>
</div>

Zwei-Wege-Bindung: $wire.entangle

Blade
<div x-data="{ query: $wire.entangle('search') }">
    <input x-model="query">
    <p x-show="query.length > 0">Suche nach: <span x-text="query"></span></p>
</div>

{{-- Verzögerte Synchronisation: nicht bei jedem Zeichen eine Anfrage --}}
<div x-data="{ query: $wire.entangle('search').live }">…</div>

Eigene Skripte innerhalb der Komponente

Blade
@script
<script>
    // Läuft einmal bei der Initialisierung der Komponente.
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    // $wire ist auch hier verfügbar.
    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Blade
{{-- Externe Bibliothek einmal für die ganze Seite laden --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets

Teilbaum ignorieren

Wenn ein Drittanbieter-Widget das DOM selbst verwaltet, stört der Morph-Algorithmus nur.

Blade
<div wire:ignore>
    <select id="select2-field">…</select>
</div>

{{-- Nur Kinder ignorieren, Attribute des Elements selbst aber aktualisieren --}}
<div wire:ignore.self>…</div>

Globale JS-Hooks

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

    // Vor dem Senden jeder Anfrage
    Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        succeed(({ status, json }) => {
            console.debug('Livewire hat geantwortet', status);
        });

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();   // CSRF-Token abgelaufen
            }
        });
    });

    // Vor und nach dem Morph eines einzelnen Elements
    Livewire.hook('morph.updated', ({ el, component }) => {});

    // Komponente wurde initialisiert
    Livewire.hook('component.init', ({ component }) => {});
});

// Programmatischer Zugriff auf Komponenten
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());

20. Testing

Livewire bringt eine eigene Test-API auf Basis von Laravel mit. Die Tests sind schnell — kein Browser nötig, es genügt PHPUnit oder Pest.

tests/Feature/CounterTest.php
<?php

use App\Livewire\Counter;
use Livewire\Livewire;

it('erhöht den Zähler', function () {
    Livewire::test(Counter::class)
        ->assertSet('count', 0)
        ->call('increment')
        ->assertSet('count', 1)
        ->call('increment')
        ->assertSet('count', 2)
        ->call('decrement')
        ->assertSet('count', 1);
});
tests/Feature/ContactFormTest.php
use App\Livewire\ContactForm;
use App\Models\Contact;
use Livewire\Livewire;

it('validiert Pflichtfelder', function () {
    Livewire::test(ContactForm::class)
        ->set('name', '')
        ->set('email', 'keine-email')
        ->call('submit')
        ->assertHasErrors([
            'name'  => 'required',
            'email' => 'email',
        ])
        ->assertNoRedirect();
});

it('speichert eine gültige Anfrage', function () {
    Livewire::test(ContactForm::class)
        ->set('name', 'Irene Kovac')
        ->set('email', 'irene@example.com')
        ->set('message', str_repeat('Wir brauchen eine Website für eine Klinik. ', 2))
        ->set('consent', true)
        ->call('submit')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

    expect(Contact::where('email', 'irene@example.com')->exists())->toBeTrue();
});

Die wichtigsten Assertions

MethodePrüft
assertSet('prop', $value)Wert einer Eigenschaft
assertNotSet('prop', $value)Dass der Wert abweicht
assertSee('Text')Text ist im Markup enthalten
assertDontSee('Text')Text fehlt
assertSeeHtml('<b>')Markup ist vorhanden
assertHasErrors(['email'])Validierungsfehler
assertHasNoErrors()Keine Fehler
assertDispatched('event')Event wurde gesendet
assertRedirect(route(…))Weiterleitung erfolgt
assertStatus(403)HTTP-Status
assertForbidden()Zugriff verweigert
assertCount('items', 3)Größe eines Arrays oder einer Collection

Authentifizierung, Parameter, Events

PHP
// Im Namen eines Benutzers
Livewire::actingAs($admin)
    ->test(OrderTable::class)
    ->assertSee('Alle Bestellungen');

// Mit Parametern für mount()
Livewire::test(OrderCard::class, ['order' => $order, 'mode' => 'full'])
    ->assertSee($order->number);

// Event empfangen
Livewire::test(OrderList::class)
    ->dispatch('order-created', orderId: 42)
    ->assertSee('Bestellung Nr. 42');

// Datei-Upload
use Illuminate\Http\UploadedFile;

Livewire::test(AvatarUploader::class)
    ->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
    ->call('save')
    ->assertHasNoErrors();

// Komponente innerhalb einer Seite
$this->get('/dashboard')
    ->assertSeeLivewire(Dashboard::class)
    ->assertDontSeeLivewire(AdminPanel::class);
Was zuerst testen. Autorisierung in Aktionen, Validierungsregeln und Zustandsübergänge. Genau dort bricht Funktionalität, und genau dort verstecken sich Sicherheitslücken.

21. Sicherheit

Grundprinzip. Jede öffentliche Methode einer Komponente ist ein offener HTTP-Endpunkt. Jede öffentliche Eigenschaft kann der Client ändern. Behandeln Sie eine Komponente wie einen Controller, nicht wie eine interne Klasse.

1. Autorisierung in jeder Aktion

PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);   // Policy — verpflichtend

    $post->delete();
}
PHP
// Autorisierung der gesamten Komponente
public function mount(Project $project): void
{
    $this->authorize('view', $project);

    $this->project = $project;
}

2. #[Locked] für Identifier

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;      // vom Frontend nicht manipulierbar

    public string $note = '';
}

Ohne #[Locked] genügt eine Zeile in der Browser-Konsole:

JavaScript (Angriff)
Livewire.find('...').set('invoiceId', 999)   // fremde Rechnung

3. Regeln für die Modellbindung

wire:model="post.title" funktioniert nur, wenn das Feld durch eine Validierungsregel freigegeben ist — so schützt Livewire vor Mass Assignment.

PHP
protected function rules(): array
{
    return [
        'post.title' => 'required|string|max:180',
        'post.body'  => 'required|string',
        // 'post.user_id' bewusst NICHT aufnehmen — sonst ließe sich der Beitrag umschreiben
    ];
}

4. Ausgabe escapen

Blade
{{-- Sicher: wird escaped --}}
{{ $comment->body }}

{{-- Gefährlich: rohes Markup vom Benutzer --}}
{!! $comment->body !!}

{{-- Wenn Markup nötig ist, serverseitig bereinigen --}}
{!! clean($comment->body) !!}

5. Rate Limiting

PHP
use Illuminate\Support\Facades\RateLimiter;

public function login(): void
{
    $key = 'login:' . request()->ip();

    if (RateLimiter::tooManyAttempts($key, 5)) {
        throw ValidationException::withMessages([
            'email' => 'Zu viele Versuche. Bitte in einer Minute erneut probieren.',
        ]);
    }

    RateLimiter::hit($key, 60);

    // …
}

6. Keine Geheimnisse in Eigenschaften

PHP
class PaymentForm extends Component
{
    public string $cardLast4 = '';        // in Ordnung

    // NICHT: landet im Klartext im Browser innerhalb von wire:snapshot
    // public string $apiSecret = '';
    // public string $fullCardNumber = '';

    protected function gateway(): PaymentGateway
    {
        return app(PaymentGateway::class);   // Geheimnisse bleiben auf dem Server
    }
}

Checkliste vor dem Release

  • $this->authorize() steht in jeder Aktion, die Daten verändert.
  • Alle Identifier sind mit #[Locked] markiert.
  • Validierungsregeln enthalten keine internen Felder (user_id, role, price).
  • Öffentliche Eigenschaften enthalten keine Tokens, Schlüssel oder überflüssigen personenbezogenen Daten.
  • Datei-Uploads sind nach MIME-Typ und Größe begrenzt.
  • Login- und Kontaktformulare sind durch Rate Limiting geschützt.
  • {!! !!} nur für vertrauenswürdiges oder bereinigtes Markup.

22. Performance

1. So wenig Zustand wie möglich in öffentlichen Eigenschaften

Schlecht
public Collection $products;   // 500 Modelle reisen zum Client und zurück

public function mount(): void
{
    $this->products = Product::with('category', 'images')->get();
}
Gut
public function render()
{
    return view('livewire.catalog', [
        'products' => Product::with('category')->paginate(24),
    ]);
}

2. Keine Anfrage bei jedem Tastendruck

Blade
{{-- Schlecht: Anfrage pro Zeichen --}}
<input wire:model.live="search">

{{-- Gut --}}
<input wire:model.live.debounce.400ms="search">

3. Schwere Seiten in Komponenten aufteilen

Die Aktualisierung einer Kindkomponente rendert das Elternteil nicht neu. Ein Dashboard aus sechs unabhängigen Widgets läuft spürbar flüssiger als ein Monolith.

4. Teure Berechnungen cachen

PHP
#[Computed(persist: true, seconds: 600)]
public function monthlyRevenue(): array
{
    return app(RevenueService::class)->byMonth();
}

5. Auf N+1 achten

PHP
// Schlecht — eine Abfrage pro Zeile in der Vorlage
$orders = Order::paginate(50);

// Gut
$orders = Order::with(['customer', 'items.product'])->paginate(50);

Aktivieren Sie in der Entwicklung den strikten Modus, damit N+1 mit einem Fehler auffällt:

app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

6. wire:key in Listen

Ohne Schlüssel baut der Morph-Algorithmus mehr Knoten um als nötig — und macht es dabei falsch.

7. Schwere Blöcke verzögert laden

Blade
<livewire:revenue-chart lazy="on-scroll" />

8. Vorsicht beim Polling

wire:poll.visible.10s statt wire:poll spart dem Server eine Größenordnung an Anfragen.

SymptomWahrscheinliche Ursache
300–800 ms Verzögerung bei jeder AktionTeures render() oder N+1
Sehr großes Seiten-MarkupModel-Collections in öffentlichen Eigenschaften
Steigende Last auf PHP-FPMAggressives wire:poll oder .live ohne Debounce
Felder „springen“ beim Aktualisieren der Listewire:key fehlt
Die ganze Seite wird neu gerendertAlles in einer Komponente, keine Aufteilung

23. Häufige Fehler und Lösungen

„Component must have a single root element“

Die Vorlage hat mehrere Wurzelknoten oder Text bzw. einen Kommentar auf oberster Ebene. Verpacken Sie alles in ein <div>.

Schlecht
<h1>Überschrift</h1>
<p>Text</p>
Gut
<div>
    <h1>Überschrift</h1>
    <p>Text</p>
</div>

„Livewire encountered corrupt data“

Die Snapshot-Signatur passt nicht. Ursachen: geänderter APP_KEY, eine aus dem Cache geladene Seite nach dem Deployment, zwei Tabs mit unterschiedlichen Sessions. Meist hilft ein Reload; nach dem Deployment php artisan optimize:clear.

Klicks funktionieren nicht, Konsole ist leer

  • @livewireScripts fehlt im Layout.
  • Eine zweite Alpine.js-Instanz ist eingebunden (Livewire 3 bringt Alpine mit).
  • Ein JS-Fehler weiter oben auf der Seite hat die Ausführung abgebrochen.
  • Das Element liegt innerhalb von wire:ignore.

Feldwerte „wandern“ zwischen Zeilen

In der Schleife fehlt wire:key bzw. :key. Der Schlüssel muss stabil und eindeutig sein.

Blade
@foreach ($rows as $row)
    <div wire:key="row-{{ $row->id }}">…</div>
@endforeach

„Unable to set component data. Public property not found“

Sie haben wire:model an eine nicht existierende oder nicht öffentliche Eigenschaft gebunden. Prüfen Sie Schreibweise und Sichtbarkeit.

„Cannot bind to model data without validation rules“

Eine Bindung wie wire:model="post.title" braucht eine Regel für post.title in rules() oder #[Validate].

Drittanbieter-Widget bricht nach dem Update

Select2, Flatpickr, TinyMCE und ähnliche verändern das DOM selbst. Verpacken Sie sie in wire:ignore und synchronisieren Sie manuell.

Blade
<div wire:ignore x-data x-init="
    const picker = flatpickr($refs.input, {
        onChange: (dates, str) => $wire.set('date', str),
    });
">
    <input x-ref="input" type="text">
</div>

Datei-Upload scheitert lautlos

Prüfen Sie upload_max_filesize und post_max_size in PHP sowie client_max_body_size in nginx. Die Werte müssen über dem Limit in der Regel max: liegen.

Modal schließt nach dem Speichern nicht

Senden Sie ein Browser-Event und verarbeiten Sie es in Alpine, statt auf ein Neurendern zu setzen.

PHP
$this->dispatch('close-modal', name: 'order-form');

Skripte laufen nach wire:navigate nicht

Verlagern Sie die Initialisierung von DOMContentLoaded nach livewire:navigated.

Fehler 419 (Page Expired)

Die Session ist abgelaufen. Erhöhen Sie SESSION_LIFETIME oder fangen Sie den Status im Hook request ab und laden Sie die Seite neu — Beispiel siehe Kapitel 19.

24. Spickzettel

Blade-Direktiven

DirektiveZweck
wire:modelFeld an Eigenschaft binden (verzögert)
wire:model.liveBindung mit Anfrage bei jeder Änderung
wire:model.blurSenden beim Fokusverlust
wire:clickMethode bei Klick aufrufen
wire:submitFormularversand verarbeiten
wire:keydown.enterAuf Taste reagieren
wire:changeAuf change reagieren
wire:confirmBestätigung vor der Aktion
wire:loadingLadeindikator
wire:targetIndikator auf eine Aktion eingrenzen
wire:dirtyEs gibt ungespeicherte Änderungen
wire:offlineKeine Verbindung
wire:pollPeriodische Aktualisierung
wire:initMethode direkt nach dem Rendern aufrufen
wire:navigateSPA-Wechsel über einen Link
wire:keyKennung eines Elements in der Schleife
wire:ignoreTeilbaum vom Morph ausnehmen
wire:transitionAnimation beim Ein- und Ausblenden
wire:streamInhalte streamen

PHP-Attribute

AttributZweck
#[Validate]Validierungsregel für eine Eigenschaft
#[Locked]Änderung vom Frontend verbieten
#[Computed]Berechnete Eigenschaft mit Cache
#[Url]Synchronisation mit dem Query-String
#[Session]Wert in der Session ablegen
#[On]Event-Listener
#[Reactive]Parameter vom Elternteil aktualisieren
#[Modelable]wire:model auf der Komponente unterstützen
#[Lazy]Komponente verzögert laden
#[Layout]Layout für Full-Page-Komponente
#[Title]Seitentitel
#[Renderless]Methode ohne Neurendern

Nützliche Methoden der Komponente

PHP
$this->reset();                       // alle Eigenschaften auf Startwerte zurücksetzen
$this->reset('search', 'page');       // ausgewählte zurücksetzen
$this->only('title', 'body');         // Array aus einem Teil der Eigenschaften
$this->except('password');            // alles außer dem Genannten
$this->fill(['title' => 'Neu']);      // Massenzuweisung
$this->pull('draft');                 // holen und zurücksetzen

$this->validate();
$this->validateOnly('email');
$this->resetValidation();
$this->addError('email', 'Meldung');

$this->dispatch('saved', id: $post->id);
$this->redirect('/orders', navigate: true);
$this->redirectRoute('orders.index', navigate: true);

$this->skipRender();                  // in diesem Zyklus nicht neu rendern
$this->js('alert("Fertig")');         // JS im Client ausführen
$this->stream(to: 'answer', content: $chunk);

Artisan-Befehle

Bash
php artisan livewire:make Orders/OrderTable
php artisan livewire:make Counter --inline
php artisan livewire:form PostForm
php artisan livewire:attribute ValidPhone
php artisan livewire:publish --config
php artisan livewire:publish --assets

Offizielle Quellen