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.
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.
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.
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.
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.
<!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>@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
php artisan livewire:publish --configDie wichtigsten Parameter in config/livewire.php:
| Parameter | Zweck |
|---|---|
class_namespace | Namespace der Komponentenklassen. Standard: App\Livewire. |
view_path | Verzeichnis der Blade-Vorlagen. |
layout | Layout für Full-Page-Komponenten. |
temporary_file_upload | Disk, Lebensdauer und Regeln für temporäre Uploads. |
inject_assets | Automatisches Einfügen von CSS/JS. Abschalten, wenn Sie die Direktiven selbst setzen. |
navigate.show_progress_bar | Fortschrittsbalken für wire:navigate. |
Installation prüfen
php artisan livewire:make Counter
# CLASS: app/Livewire/Counter.php
# VIEW: resources/views/livewire/counter.blade.php3. Die erste Komponente
Eine Komponente besteht aus zwei Dateien: Klasse und Vorlage.
<?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');
}
}<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><div>.
Drei Wege, eine Komponente einzubinden
{{-- 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.
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class)->middleware('auth')->name('dashboard');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.
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.
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.
| Erlaubt | Nicht 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 |
Initialisierung: mount()
mount() ist der Konstruktor der Komponente. Die Methode läuft einmal beim ersten Rendern
und erhält die aus Blade übergebenen Parameter.
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;
}
}<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.
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.
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).
{{-- 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
| Modifikator | Verhalten | Einsatz |
|---|---|---|
| — | Verzögertes Senden | Normale Formularfelder |
.live | Anfrage bei jeder Änderung | Live-Suche, abhängige Selects |
.blur | Anfrage beim Fokusverlust | Feldvalidierung beim Verlassen |
.debounce.Xms | Auf Eingabepause warten | Suche während des Tippens |
.throttle.Xs | Höchstens einmal pro Intervall | Teure Abfragen |
.number | In Zahl umwandeln | Numerische Felder |
.boolean | In Boolean umwandeln | Ja/Nein-Selects |
.fill | Startwert aus dem Markup übernehmen | Vorausgefüllte Formulare |
Alle Feldtypen
{{-- 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.
public array $form = [
'name' => '',
'address' => ['city' => '', 'street' => ''],
];
public Post $post;<input wire:model="form.name">
<input wire:model="form.address.city">
<input wire:model="post.title">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.
{{-- 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
<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>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');
}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.
public function archive(Post $post): void
{
$this->authorize('update', $post);
$post->update(['archived_at' => now()]);
}Modifikatoren für Aktionen
{{-- 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
| Aktion | Wirkung |
|---|---|
$refresh | Komponente 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 |
<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
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
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ügtFolgeaktualisierung
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.
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.
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
| Eigenschaft | Hook |
|---|---|
$price | updatedPrice() |
$isActive | updatedIsActive() |
$form['email'] | updatedFormEmail() |
$post->title | updatedPostTitle() |
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)
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.
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
public function updated(string $property): void
{
$this->validateOnly($property); // nur das geänderte Feld prüfen
}<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
$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.
php artisan livewire:form PostForm<?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'));
}
}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);
}
}<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.
<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.
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
<button wire:click="$parent.refreshList()">Liste aktualisieren</button>Zwei-Wege-Bindung: #[Modelable]
Erlaubt wire:model direkt auf der Komponente — praktisch für Eingabe-Widgets.
use Livewire\Attributes\Modelable;
class RatingInput extends Component
{
#[Modelable]
public int $value = 0;
public function set(int $value): void
{
$this->value = $value;
}
}{{-- In der Elternvorlage --}}
<livewire:rating-input wire:model.live="review.rating" />Bedingtes Rendern von Kindkomponenten
@if ($showDetails)
<livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif11. 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
// 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();{{-- Direkt aus der Vorlage --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">Neue Bestellung</button>Empfangen
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):
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.
$this->dispatch('notify', type: 'success', message: 'Bestellung gespeichert');<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>document.addEventListener('notify', (event) => {
console.log(event.detail.message);
});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.
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.
}
}<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>$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
// 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
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.
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.
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
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
public function save(): void
{
$this->validate();
Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);
session()->flash('status', 'Einstellungen gespeichert');
}@if (session('status'))
<div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif#[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.
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);
}
}<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
<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
'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
],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.
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),
]);
}
}<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
// 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'),
]);
}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.
{{-- 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
{{-- 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
<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
<div wire:offline class="banner banner--warn">
Keine Verbindung zum Server. Änderungen werden nicht gespeichert.
</div>Skeleton beim ersten Laden
<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.
<livewire:revenue-chart lazy />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.
{{-- Laden, sobald der Block im Viewport erscheint --}}
<livewire:revenue-chart lazy="on-scroll" />Verzögerte Initialisierung: wire:init
<div wire:init="loadStats">
@if ($stats)
…
@else
<div class="skeleton"></div>
@endif
</div>Server abfragen: wire:poll
{{-- 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>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.
<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
<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
@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{{-- Externe Bibliothek einmal für die ganze Seite laden --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassetsTeilbaum ignorieren
Wenn ein Drittanbieter-Widget das DOM selbst verwaltet, stört der Morph-Algorithmus nur.
<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
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.
<?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);
});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
| Methode | Prü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
// 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);21. Sicherheit
1. Autorisierung in jeder Aktion
public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post); // Policy — verpflichtend
$post->delete();
}// Autorisierung der gesamten Komponente
public function mount(Project $project): void
{
$this->authorize('view', $project);
$this->project = $project;
}2. #[Locked] für Identifier
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:
Livewire.find('...').set('invoiceId', 999) // fremde Rechnung3. 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.
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
{{-- 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
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
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
public Collection $products; // 500 Modelle reisen zum Client und zurück
public function mount(): void
{
$this->products = Product::with('category', 'images')->get();
}public function render()
{
return view('livewire.catalog', [
'products' => Product::with('category')->paginate(24),
]);
}2. Keine Anfrage bei jedem Tastendruck
{{-- 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
#[Computed(persist: true, seconds: 600)]
public function monthlyRevenue(): array
{
return app(RevenueService::class)->byMonth();
}5. Auf N+1 achten
// 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:
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
<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.
| Symptom | Wahrscheinliche Ursache |
|---|---|
| 300–800 ms Verzögerung bei jeder Aktion | Teures render() oder N+1 |
| Sehr großes Seiten-Markup | Model-Collections in öffentlichen Eigenschaften |
| Steigende Last auf PHP-FPM | Aggressives wire:poll oder .live ohne Debounce |
| Felder „springen“ beim Aktualisieren der Liste | wire:key fehlt |
| Die ganze Seite wird neu gerendert | Alles 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>.
<h1>Überschrift</h1>
<p>Text</p><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
@livewireScriptsfehlt 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.
@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.
<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.
$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
| Direktive | Zweck |
|---|---|
wire:model | Feld an Eigenschaft binden (verzögert) |
wire:model.live | Bindung mit Anfrage bei jeder Änderung |
wire:model.blur | Senden beim Fokusverlust |
wire:click | Methode bei Klick aufrufen |
wire:submit | Formularversand verarbeiten |
wire:keydown.enter | Auf Taste reagieren |
wire:change | Auf change reagieren |
wire:confirm | Bestätigung vor der Aktion |
wire:loading | Ladeindikator |
wire:target | Indikator auf eine Aktion eingrenzen |
wire:dirty | Es gibt ungespeicherte Änderungen |
wire:offline | Keine Verbindung |
wire:poll | Periodische Aktualisierung |
wire:init | Methode direkt nach dem Rendern aufrufen |
wire:navigate | SPA-Wechsel über einen Link |
wire:key | Kennung eines Elements in der Schleife |
wire:ignore | Teilbaum vom Morph ausnehmen |
wire:transition | Animation beim Ein- und Ausblenden |
wire:stream | Inhalte streamen |
PHP-Attribute
| Attribut | Zweck |
|---|---|
#[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
$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
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 --assetsOffizielle Quellen
- Livewire-Dokumentation: livewire.laravel.com/docs
- Laravel-Dokumentation: laravel.com/docs
- Alpine.js: alpinejs.dev