Livewire 4 — vollständiger Leitfaden
Livewire 4 behält das bekannte Komponentenmodell und baut alles darum herum neu: Single-File-Komponenten, Islands mit unabhängigem Rendern, Slots im Kontext des Elternteils, eingebautes Drag and Drop und clientseitige Direktiven, die den Server gar nicht erst behelligen. Dieser Leitfaden deckt das Framework vollständig ab und benennt die Unterschiede zu Version 3 dort, wo sie wehtun.
1. Was Livewire 4 ändert
Livewire 4 ist das größte Release in der Geschichte des Frameworks. Das Komponentenmodell selbst hat sich nicht geändert: PHP-Klasse, Blade-Vorlage, Zustand auf dem Server, Markup über die Leitung. Geändert hat sich alles drumherum: wo Komponenten liegen, wie sie geschrieben werden und wie viel einer Seite ein einzelnes Update berührt.
Die fünf wichtigsten Punkte
| Neuerung | Was sie bringt |
|---|---|
| Single-File-Komponenten | Klasse und Markup in einer .blade.php. Kein Springen mehr zwischen zwei Verzeichnissen für eine Komponente mit zwanzig Zeilen. |
| Islands | Isolierte Bereiche innerhalb einer Komponente, die sich eigenständig neu rendern. Ein Umsatzzähler stößt nicht länger die Abfragen des gesamten Dashboards an. |
| Slots | Eltern reichen Markup in Kinder hinein, ausgewertet im Kontext des Elternteils: wire:click im Slot ruft die Methode des Elternteils auf. |
| Optimistisches UI | wire:show, wire:text, wire:bind ändern das DOM sofort, ohne Serveraufruf. |
| Drag and Drop | wire:sort ist eingebaut. Kein SortableJS, kein Klebecode. |
Ist das ein Neuschreiben der Anwendung?
Nein. Livewire 4 bleibt weitgehend abwärtskompatibel: klassenbasierte Komponenten funktionieren weiter. Single-File ist der Standard für neue Komponenten, keine erzwungene Migration. Es gibt eine Reihe von Breaking Changes, die man vor dem Upgrade lesen sollte — sie sind im Vergleich Livewire 3 und 4 gesammelt, und der offizielle Upgrade Guide bleibt die maßgebliche Quelle.
wire:model-Semantik, Islands und Slots. Der Rest des
Komponentenmodells wird vertraut wirken.
2. Installation und Voraussetzungen
composer require livewire/livewire:^4.0
php artisan optimize:clear
Wie in Version 3 werden die Assets automatisch eingebunden. @livewireStyles und
@livewireScripts müssen Sie nur bei einem abweichenden Layout oder strenger Content
Security Policy von Hand setzen.
/livewire/ wurde /livewire-{hash}/. Firewall-Regeln, WAF-Ausnahmen,
CDN-Bypässe oder nginx-Location-Blöcke, die auf den wörtlichen Pfad /livewire/ zeigen,
müssen auf das neue Muster erweitert werden — sonst wirkt die Anwendung in der Produktion kaputt,
während sie auf dem Laptop einwandfrei läuft.
Konfiguration veröffentlichen
php artisan livewire:publish --config| Einstellung | Zweck |
|---|---|
component_locations | Verzeichnisse, die nach Komponenten durchsucht werden. Standard: resources/views/components und resources/views/livewire. |
component_namespaces | Benannte Wurzeln, etwa pages::. |
component_layout | Hieß in v3 layout. Nutzt den Namespace layouts::. |
component_placeholder | Hieß in v3 lazy_placeholder. |
make_command | Was make:livewire erzeugt: Single-File oder klassenbasiert. |
smart_wire_keys | Jetzt standardmäßig true. |
csp_safe | Build, der strengere CSP erfüllt. |
// Weiterhin klassenbasierte Komponenten erzeugen
'make_command' => [
'type' => 'class',
],3. Single-File-Komponenten
Die auffälligste Änderung im Alltag. Eine Komponente ist jetzt eine Blade-Datei, die mit einem PHP-Block samt anonymer Klasse beginnt und danach das Markup enthält.
php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php<?php
use Livewire\Component;
use App\Models\Post;
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function save()
{
$this->validate();
Post::create([
'title' => $this->title,
'body' => $this->body,
'user_id' => auth()->id(),
]);
$this->reset();
$this->dispatch('notify', message: 'Beitrag erstellt');
}
};
?>
<div>
<form wire:submit="save">
<input type="text" wire:model="title" placeholder="Titel">
@error('title') <span class="error">{{ $message }}</span> @enderror
<textarea wire:model="body" placeholder="Text"></textarea>
@error('body') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Speichern</button>
</form>
</div>
Öffentliche Eigenschaften stehen im Markup als gewöhnliche Variablen bereit —
{{ $title }} funktioniert genauso wie zuvor in einer separaten Vorlagendatei.
Zu dem Emoji
Der Standarddateiname trägt das Präfix ⚡. Es ist eine optische Markierung, die Livewire-
von gewöhnlichen Blade-Komponenten im selben Verzeichnis unterscheidet, und sie ist
optional — in config/livewire.php lässt sie sich abschalten.
Multi-File-Komponenten
php artisan make:livewire post.create --mfcKlassenbasierte Komponenten funktionieren weiter
<?php
namespace App\Livewire;
use Livewire\Component;
class CreatePost extends Component
{
public string $title = '';
public function render()
{
return view('livewire.create-post');
}
}4. Ablage, Benennung und Namespaces
resources/views/
├── components/
│ ├── ⚡counter.blade.php → <livewire:counter />
│ ├── post/
│ │ ├── ⚡create.blade.php → <livewire:post.create />
│ │ └── ⚡index.blade.php → <livewire:post.index />
│ └── button.blade.php (gewöhnliche Blade-Komponente)
└── pages/
└── ⚡dashboard.blade.php → <livewire:pages::dashboard />{{-- Nach Name --}}
<livewire:counter />
{{-- Verschachtelte Verzeichnisse mit Punkten --}}
<livewire:post.create />
{{-- Mit Namespace --}}
<livewire:pages::post.create />
{{-- Mit Props --}}
<livewire:post.create :title="$initialTitle" :author="$user" /><livewire:some-component> schlicht gar nichts — keine Fehlermeldung, keine Ausgabe.
Immer selbstschließend notieren: <livewire:some-component />. Das ist die häufigste
Überraschung beim Übernehmen von Blade-Dateien aus v3.
Routing auf eine Komponente
use App\Livewire\Dashboard;
// Nach Klasse
Route::livewire('/dashboard', Dashboard::class);
// Nach Komponentenname
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');Layout
'component_layout' => 'layouts::app',use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
new class extends Component {
#[Layout('layouts::app')]
#[Title('Übersicht')]
public function render() { /* … */ }
};5. Eigenschaften und Props
new class extends Component {
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
};Props vom Elternteil: #[Prop]
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
#[Prop]
public bool $dismissible = false;
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
@if ($dismissible)
<button wire:click="$dispatch('dismiss')">×</button>
@endif
</div><livewire:alert type="warning" dismissible class="mt-4">
Die Rechnung ist überfällig.
</livewire:alert>mount() läuft weiterhin zuerst
use App\Models\Order;
new class extends Component {
public Order $order;
public string $mode;
public function mount(Order $order, string $mode = 'compact'): void
{
$this->order = $order;
$this->mode = $mode;
}
};#[Locked] ist genauso wichtig
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $invoiceId; // der Client kann keine fremde id unterschieben
public string $note = '';
};6. wire:model in Version 4
wire:model
verhalten sich anders, und beide scheitern lautlos statt mit einer Fehlermeldung.
Änderung 1: Modifikatoren steuern die clientseitige Synchronisation
In v3 entschieden .blur und .change nur, wann eine Netzwerkanfrage
ausgelöst wird. In v4 entscheiden sie, wann der Wert überhaupt synchronisiert — auch auf
dem Client. Für das alte Verhalten ergänzen Sie .live.
{{-- Livewire 3 --}}
<input wire:model.blur="title">
{{-- Entspricht in v4 dem gleichen Verhalten --}}
<input wire:model.live.blur="title">Änderung 2: kein Event-Bubbling von Kindelementen mehr
{{-- Livewire 3: der Wrapper fing das Event des Feldes ab --}}
<div wire:model="value">
<input type="text">
</div>
{{-- Livewire 4: ausdrücklich aktivieren --}}
<div wire:model.deep="value">
<input type="text">
</div>Der Rest bleibt gleich
{{-- Standardmäßig verzögert --}}
<input type="text" wire:model="name">
{{-- Anfrage bei jeder Änderung --}}
<input type="text" wire:model.live="search">
{{-- Live-Suche mit Verzögerung --}}
<input type="text" wire:model.live.debounce.400ms="search">
{{-- Verschachtelte Daten --}}
<input wire:model="form.address.city">
<input wire:model="post.title">post.title) verlangt weiterhin eine passende
Validierungsregel. Das ist der Schutz vor Mass Assignment und ist unverändert nach v4 übernommen.
7. Aktionen, Async und Renderless
<button wire:click="save">Speichern</button>
<form wire:submit="save">…</form>
<button wire:click="delete({{ $post->id }})"
wire:confirm="Beitrag löschen? Das lässt sich nicht rückgängig machen.">Löschen</button>Aktionen ohne Neurendern
use Livewire\Attributes\Renderless;
new class extends Component {
#[Renderless]
public function incrementViewCount(): void
{
$this->post->increment('views');
}
};<button type="button" wire:click.renderless="incrementViewCount">Zählen</button>Asynchrone Aktionen
#[Async] (oder der Modifikator .async) führt eine Aktion
parallel aus, außerhalb der normalen Anfrage-Warteschlange. Ideal für
Fire-and-forget: Analytics, Logging, Cache-Vorwärmung.
use Livewire\Attributes\Async;
new class extends Component {
#[Async]
public function logInteraction(string $element): void
{
Analytics::record($element, auth()->id());
}
};<button wire:click.async="logInteraction('cta-hero')">Loslegen</button><button wire:click="$refresh">Aktualisieren</button>
<button wire:click="$set('tab', 'settings')">Einstellungen</button>
<button wire:click="$toggle('showFilters')">Filter</button>
<button wire:click="$parent.closeModal()">Schließen</button>8. Lebenszyklus und Hooks
1. Komponenteninstanz wird erzeugt
2. boot()
3. mount($params)
4. booted()
5. render() 1. POST /livewire-{hash}/update trifft mit dem Zustands-Snapshot ein
2. Prüfsumme des Snapshots wird verifiziert
3. Komponenteninstanz wird erzeugt
4. boot()
5. Zustand wird aus dem Snapshot wiederhergestellt
6. hydrate() / hydrateFoo()
7. booted()
8. updating($prop, $value) / updatingFoo($value)
9. Eigenschaften erhalten neue Werte
10. updated($prop, $value) / updatedFoo($value)
11. Aktionen aus der Warteschlange laufen
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — Zustand wird zurück in den Snapshot gepackt
14. Antwort: Markup + effectsnew class extends Component {
public float $price = 0;
public function boot(): void {}
public function booted(): void {}
public function hydrate(): void {}
public function dehydrate(): void {}
public function updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
};9. Validierung und Form-Objekte
use Livewire\Attributes\Validate;
new class extends Component {
#[Validate('required|string|min:2|max:80')]
public string $name = '';
#[Validate('required|email:rfc,dns')]
public string $email = '';
#[Validate('accepted', message: 'Die Einwilligung zur Datenverarbeitung ist erforderlich.')]
public bool $consent = false;
public function submit(): void
{
$data = $this->validate();
Contact::create($data);
$this->reset();
}
};protected function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->userId),
],
'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
];
}Form-Objekte
<?php
namespace App\Livewire\Forms;
use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
public function store(): Post
{
$this->validate();
return Post::create($this->except('post'));
}
public function update(): void
{
$this->validate();
$this->post->update($this->except('post'));
}
}Fehler in JavaScript
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>10. Islands
Islands sind das Aushängeschild von Livewire 4 und die Neuerung, die das Denken über Komponentengröße verändert. Ein Island ist ein Bereich innerhalb einer Komponente, der sich unabhängig neu rendert: bei einem Update wird nur dieses Fragment neu berechnet und gepatcht, nicht die ganze Komponente.
@island
<div>Umsatz: {{ $this->revenue }}</div>
@endislandWarum das zählt
In v3 war das Standardmittel gegen „dieses Dashboard ist langsam“ die Aufteilung in sechs Kindkomponenten, damit ein Widget-Update nicht die Abfragen der anderen fünf erneut ausführt. Islands liefern dieselbe Isolation ohne Komponentengrenze: eine Komponente, eine Klasse, mehrere unabhängig aktualisierende Bereiche.
<?php
use Livewire\Component;
use Livewire\Attributes\Computed;
new class extends Component {
#[Computed]
public function revenue()
{
return Order::whereMonth('created_at', now()->month)->sum('total');
}
#[Computed]
public function queue()
{
return Job::pending()->count();
}
};
?>
<div>
@island(name: 'revenue')
<div class="card">
<h3>Umsatz diesen Monat</h3>
<p class="stat">{{ number_format($this->revenue, 2, ',', '.') }} €</p>
</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">
<h3>Offene Jobs</h3>
<p class="stat">{{ $this->queue }}</p>
</div>
@endisland
@island(name: 'feed', lazy: true)
@placeholder
<div class="card skeleton animate-pulse h-64"></div>
@endplaceholder
<div class="card">…</div>
@endisland
</div>Optionen
| Option | Wirkung |
|---|---|
name | Benennt das Island, damit Aktionen und JavaScript es adressieren können. Mehrere Islands mit gleichem Namen rendern immer als Gruppe. |
lazy | Rendert, sobald das Island in den Viewport scrollt. |
defer | Rendert direkt nach dem Laden der Seite, unabhängig von der Sichtbarkeit. |
always | Erzwingt ein Update bei jedem Rendern des Elternteils. |
skip | Überspringt das initiale Rendern. |
Ein Island aus einer Aktion adressieren
@island(name: 'revenue')
Umsatz: {{ $this->revenue }}
@endisland
<button wire:click="$refresh" wire:island="revenue">Umsatz aktualisieren</button>Append und Prepend — Endlosliste ohne Verkabelung
@island(name: 'feed')
@foreach ($this->posts as $post)
<article wire:key="post-{{ $post->id }}">{{ $post->title }}</article>
@endforeach
@endisland
<button wire:click="loadMore" wire:island.append="feed">Mehr laden</button><button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
Mehr laden
</button>Polling auf ein Island beschränkt
@island(name: 'queue')
<div wire:poll.3s>
Offene Jobs: {{ $this->queue }}
</div>
@endisland11. Slots und Attribut-Weitergabe
Livewire-Komponenten nehmen jetzt Slot-Inhalte entgegen, wie Blade-Komponenten es immer konnten — mit
einer wichtigen Besonderheit: Slot-Inhalt wird im Kontext des Elternteils ausgewertet.
Ein wire:click im Slot ruft die Methode des Elternteils auf, nicht die des Kindes.
Standard-Slot
<livewire:modal>
<h2>Beitrag erstellen</h2>
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Speichern</button>
</form>
</livewire:modal><?php
use Livewire\Component;
new class extends Component {
public bool $isOpen = false;
public function toggle(): void
{
$this->isOpen = ! $this->isOpen;
}
};
?>
<div wire:show="isOpen" class="modal">
<button wire:click="toggle" class="modal__close">×</button>
<div class="modal__body">
{{ $slot }}
</div>
</div>Benannte Slots
<livewire:modal>
<wire:slot name="header">
<h2>Beitrag erstellen</h2>
</wire:slot>
<form wire:submit="save">
<input wire:model="title">
</form>
<wire:slot name="footer">
<button wire:click="save">Speichern</button>
</wire:slot>
</livewire:modal><div class="modal">
@if ($header = $slot('header'))
<div class="modal__header">{{ $header }}</div>
@endif
<div class="modal__body">{{ $slot }}</div>
@if ($footer = $slot('footer'))
<div class="modal__footer">{{ $footer }}</div>
@endif
</div>Attribut-Weitergabe
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $type = 'info';
};
?>
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $slot }}
</div><livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
Diese Rechnung ist 14 Tage überfällig.
</livewire:alert>12. Verschachtelte Komponenten und wire:key
<div>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>Smart Wire Keys
In Version 4 ist smart_wire_keys standardmäßig aktiv, sodass Livewire Keys in vielen
Schleifensituationen selbst ableitet, in denen zuvor ein expliziter nötig war.
wire:key mit der Datensatz-id
tragen.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeachuse Livewire\Attributes\Reactive;
new class extends Component {
#[Reactive]
public int $quantity;
};<button wire:click="$parent.refreshList()">Liste aktualisieren</button>use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public int $value = 0;
};13. Events
Die Event-API ist gegenüber Version 3 unverändert.
$this->dispatch('post-created');
$this->dispatch('post-created', postId: $post->id, title: $post->title);
$this->dispatch('refresh')->to(OrderList::class);
$this->dispatch('recalculate')->self();use Livewire\Attributes\On;
new class extends Component {
#[On('post-created')]
public function onPostCreated(int $postId, string $title): void
{
// …
}
};$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>14. Berechnete Eigenschaften
use Livewire\Attributes\Computed;
new class extends Component {
public array $items = [];
#[Computed]
public function products()
{
return Product::whereIn('id', array_keys($this->items))->get();
}
#[Computed]
public function subtotal(): float
{
return $this->products->sum(
fn ($product) => $product->price * $this->items[$product->id]
);
}
};@island(name: 'totals')
<p>Zwischensumme: {{ number_format($this->subtotal, 2, ',', '.') }} €</p>
@endisland#[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'),
];
}$this->products, niemals
$products.
15. Zustand in URL und Session
use Livewire\Attributes\Url;
use Livewire\Attributes\Session;
new class extends Component {
#[Url]
public string $search = '';
#[Url(as: 'cat')]
public ?int $categoryId = null;
#[Url(except: '')]
public string $sort = 'popular';
#[Url(keep: true)]
public int $perPage = 24;
#[Session]
public bool $sidebarCollapsed = false;
};public function save(): void
{
$this->validate();
session()->flash('status', 'Einstellungen gespeichert');
}#[Url]-Eigenschaft ruft bei Änderung die History-API auf. Bei Feldern, die sich pro
Tastendruck ändern, kombinieren Sie das mit wire:model.live.debounce.
16. Datei-Uploads
<?php
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
new class extends Component {
use WithFileUploads;
#[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]
public $avatar;
public function save(): void
{
$this->validate();
$path = $this->avatar->store('avatars', 'public');
auth()->user()->update(['avatar_path' => $path]);
$this->reset('avatar');
}
};
?>
<div>
<form wire:submit="save">
<input type="file" wire:model="avatar" accept="image/*">
<div wire:loading wire:target="avatar">Wird hochgeladen…</div>
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Vorschau" class="preview">
@endif
@error('avatar') <p class="error">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Speichern</button>
</form>
</div><div x-data="{ progress: 0, uploading: false }"
x-on:livewire-upload-start="uploading = true"
x-on:livewire-upload-finish="uploading = false; progress = 0"
x-on:livewire-upload-progress="progress = $event.detail.progress">
<input type="file" wire:model="avatar">
<div x-show="uploading" class="progress">
<div class="progress__bar" :style="`width: ${progress}%`"></div>
</div>
</div>upload_max_filesize und post_max_size in PHP sowie
client_max_body_size in nginx müssen Ihre max:-Regel übersteigen.
17. Paginierung
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function with(): array
{
return [
'orders' => Order::query()
->with('customer')
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->paginate(25),
];
}
};Islands machen aus Paginierung eine Endlosliste
<div>
<input type="search" wire:model.live.debounce.400ms="search">
@island(name: 'rows')
@foreach ($orders as $order)
<div wire:key="order-{{ $order->id }}">{{ $order->number }}</div>
@endforeach
@endisland
<button wire:click="nextPage" wire:island.append="rows">Mehr laden</button>
</div>paginate() ein zusätzliches COUNT(*).
simplePaginate() ist spürbar günstiger.
18. Ladezustände
<div wire:loading>Wird geladen…</div>
<div wire:loading.remove>Inhalt</div>
<button wire:click="save">Speichern</button>
<span wire:loading wire:target="save">Wird gespeichert…</span>
<button wire:click="save" wire:loading.attr="disabled">Speichern</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Speichern</button>
<div wire:loading.delay>Wird geladen…</div>
<div wire:loading.delay.long>Wird geladen…</div>Styling aus CSS
[data-loading] .btn {
opacity: 0.5;
pointer-events: none;
}<input wire:model="title">
<span wire:dirty wire:target="title">Es gibt ungespeicherte Änderungen</span>
<div wire:offline class="banner banner--warn">
Keine Verbindung zum Server.
</div>19. Optimistisches UI auf dem Client
Eine ganze Klasse von Interaktionen brauchte den Server nie: ein Panel aufklappen, einen Zeichenzähler anzeigen, ein Feld rot färben. Version 4 gibt ihnen vollwertige Direktiven, die das DOM sofort ändern — ganz ohne Anfrage.
{{-- Schaltet die Sichtbarkeit per CSS, sofort --}}
<div wire:show="showModal" class="modal">…</div>
<button wire:click="$toggle('showModal')">Öffnen</button>{{-- Der Text folgt der Eigenschaft auf dem Client --}}
<span wire:text="title"></span>
<input wire:model="title"><textarea wire:model="message" maxlength="280"></textarea>
<span wire:text="message.length"></span> / 280
<span wire:bind:class="message.length > 240 && 'text-red-500'">
Wird lang
</span>20. Drag and Drop
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
{{ $task->title }}
</li>
@endforeach
</ul>new class extends Component {
public function reorder(array $order): void
{
foreach ($order as $position => $id) {
Task::where('id', $id)->update(['position' => $position]);
}
}
};Anfasser
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
<span wire:sort:handle class="cursor-grab">⠿</span>
<span>{{ $task->title }}</span>
<button wire:sort:ignore wire:click="delete({{ $task->id }})">Löschen</button>
</li>
@endforeach
</ul>Zwischen Listen ziehen
<div class="board">
@foreach ($columns as $column)
<ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
@foreach ($column->cards as $card)
<li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
{{ $card->title }}
</li>
@endforeach
</ul>
@endforeach
</div>21. Scoped CSS und Skripte
<?php
use Livewire\Component;
use Livewire\Attributes\Prop;
new class extends Component {
#[Prop]
public string $plan = 'starter';
};
?>
<div class="card">
<h3 class="title">{{ ucfirst($plan) }}</h3>
<p class="price">{{ $this->price }} € / Monat</p>
</div>
<style>
/* Auf diese Komponente begrenzt — keine globale Kollision bei .title */
.card { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
.title { font-size: 20px; font-weight: 700; }
.price { color: #2d6a4f; }
</style><style global>
:root { --brand: #f53004; }
</style>@script
<script>
const chart = new Chart(document.getElementById('sales'), {
type: 'line',
data: @json($chartData),
});
$wire.on('data-updated', ({ points }) => {
chart.data.datasets[0].data = points;
chart.update();
});
</script>
@endscript23. JavaScript-Integration
<div x-data="{ open: false }">
<button x-on:click="open = !open">Details</button>
<div x-show="open">
<p x-text="$wire.title"></p>
<button x-on:click="$wire.save()">Speichern</button>
<button x-on:click="await $wire.calculate(); open = false">Berechnen</button>
</div>
</div>wire:ref
<livewire:modal wire:ref="modal" />
<button x-on:click="$refs.modal.open()">Modal öffnen</button>#[Json]
use Livewire\Attributes\Json;
new class extends Component {
#[Json]
public function searchSuggestions(string $term): array
{
return Product::search($term)->take(5)->pluck('name')->all();
}
};Interceptors
document.addEventListener('livewire:init', () => {
Livewire.interceptRequest(({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload();
}
});
});
Livewire.interceptMessage(({ component, succeed }) => {
succeed(() => {
console.debug('Komponente aktualisiert', component.name);
});
});
});<div wire:ignore>
<select id="select2-field">…</select>
</div>24. Testing
Pest ist der empfohlene Weg, Livewire-4-Komponenten zu testen.
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.phpuse Livewire\Livewire;
it('rendert', function () {
Livewire::test('post.create')
->assertStatus(200);
});
it('validiert Pflichtfelder', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('speichert einen gültigen Beitrag', function () {
Livewire::actingAs($user)
->test('post.create')
->set('title', 'Ein durchaus vernünftiger Titel')
->set('body', str_repeat('Ausreichend langer Beitragstext. ', 3))
->call('save')
->assertHasNoErrors()
->assertDispatched('notify');
});| Methode | Prüft |
|---|---|
assertSet() / assertNotSet() | Eigenschaftswerte |
assertSee() / assertDontSee() | Ausgabe |
assertHasErrors() / assertHasNoErrors() | Validierung |
assertDispatched() | Events |
assertRedirect() | Weiterleitungen |
assertForbidden() | Autorisierung |
25. Sicherheit und Performance
Sicherheits-Checkliste
$this->authorize()in jeder Aktion, die Daten verändert.- Alle Identifier mit
#[Locked]markiert. - Hilfsmethoden als
protectedoderprivatedeklariert. - Validierungsregeln ohne interne Felder (
user_id,role,price). - Keine Tokens oder Schlüssel in öffentlichen Eigenschaften — der Snapshot geht im Klartext in den Browser.
- Uploads nach MIME-Typ und Größe begrenzt.
- Login-Formulare hinter einem Rate Limiter.
- Optimistisches UI gilt nie als maßgeblich.
Performance
| Symptom | Lösung in v4 |
|---|---|
| Ein Widget-Update stößt alle Abfragen der Seite an | Bereiche in @island einpacken |
| Riesiges Seiten-Markup | Model-Collections aus öffentlichen Eigenschaften herausnehmen |
| Eine Anfrage pro Tastendruck | wire:model.live.debounce.400ms |
| Last durch Polling | Polling im Island plus .visible |
| Round Trip nur zum Umschalten eines Panels | wire:show statt einer Aktion |
| Zähler-Inkrement rendert die Seite neu | #[Renderless] |
| N+1-Abfragen | Eager Loading über with() |
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}26. Häufige Fehler und Spickzettel
Die Komponente rendert überhaupt nicht
Der Tag ist nicht selbstschließend. Schreiben Sie <livewire:my-component />.
In der Produktion alles 404, lokal läuft es
Der Livewire-Endpunkt ist von /livewire/ nach /livewire-{hash}/ gewandert.
Eine Firewall-Regel, WAF-Ausnahme oder nginx-Location blockiert die Update-Anfragen.
Ein Feld synchronisiert nach dem Upgrade nicht mehr
Ergänzen Sie .live: wire:model.live.blur="title".
Ein Wrapper greift sein Feld nicht mehr ab
Verwenden Sie wire:model.deep.
wire:transition-Modifikatoren tun nichts
Sie wurden entfernt — v4 nutzt die native View Transitions API. Die Animation gehört ins CSS.
Ein Island aktualisiert nie
- Das Island hat keinen
name, die Aktion adressiert aber einen. wire:island="…"zeigt auf einen Namen, den es in der Ausgabe nicht gibt.- Der Bereich hängt an Elternzustand, der sich gar nicht geändert hat —
always: trueergänzen.
Volt-Komponenten funktionieren nicht mehr
Volt ist in den Kern gewandert. Ersetzen Sie Livewire\Volt\Component durch
Livewire\Component, Volt::route() durch Route::livewire() und
entfernen Sie Service Provider und Paket.
Direktiven-Spickzettel
| Direktive | Zweck | Neu in v4 |
|---|---|---|
wire:model | Feld an Eigenschaft binden | Semantik geändert |
wire:model.deep | Events von Kindelementen abfangen | ja |
wire:click.async | Aktion parallel ausführen | ja |
wire:click.renderless | Neurendern überspringen | ja |
wire:island | Ein Island adressieren | ja |
wire:island.append | An ein Island anhängen | ja |
wire:show | Sichtbarkeit auf dem Client schalten | ja |
wire:text | Text clientseitig binden | ja |
wire:bind | Attribut reaktiv binden | ja |
wire:sort | Drag and Drop | ja |
wire:intersect | Aktion beim Eintritt in den Viewport | ja |
wire:ref | Element für JS benennen | ja |
wire:navigate:scroll | Container-Scroll bewahren | umbenannt |
wire:poll | Periodische Aktualisierung | island-begrenzt |
wire:key | Element-Identität in Schleifen | Smart Keys standardmäßig |
Blade-Direktiven
| Direktive | Zweck |
|---|---|
@island … @endisland | Unabhängig rendernder Bereich |
@placeholder … @endplaceholder | Platzhalter für ein Lazy-Island |
<wire:slot name="…"> | Inhalt eines benannten Slots |
@script … @endscript | Komponenten-eigenes JavaScript |
@assets … @endassets | Seiten-Assets, einmalig geladen |
<style> / <style global> | Scoped / globales CSS |
PHP-Attribute
| Attribut | Zweck |
|---|---|
#[Validate] / #[Locked] | Validierung und Schreibschutz |
#[Computed] | Gecachter abgeleiteter Wert |
#[Url] / #[Session] | Zustand erhalten |
#[On] / #[Prop] | Events und Props |
#[Async] / #[Renderless] | Parallelität und Render-Verzicht |
#[Json] | Daten direkt an JavaScript |
php artisan make:livewire post.create # Single-File
php artisan make:livewire post.create --mfc # Multi-File
php artisan make:livewire post.create --test # mit Test
php artisan livewire:publish --config
php artisan optimize:clearOffizielle Quellen
- Livewire-Dokumentation: livewire.laravel.com/docs
- Upgrade Guide: livewire.laravel.com/docs/upgrading
- Alpine.js: alpinejs.dev