Livewire 4 — kompletný sprievodca

Livewire 4 si zachováva známy model komponentu a prestavuje všetko okolo neho: jednosúborové komponenty, ostrovy s nezávislým vykresľovaním, sloty v kontexte rodiča, vstavaný drag and drop a klientské direktívy, ktoré vôbec nechodia na server. Sprievodca pokrýva framework celý, s výslovným označením rozdielov štvrtej verzie.

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

1. Čo mení Livewire 4

Livewire 4 je najväčšie vydanie v histórii frameworku. Samotný model komponentu sa nezmenil: PHP trieda, Blade šablóna, stav na serveri, HTML po drôte. Zmenilo sa všetko okolo: kde komponenty žijú, ako sa píšu a akú časť stránky zasiahne jedna aktualizácia.

Päť najdôležitejších vecí

NovinkaČo prináša
Jednosúborové komponenty Trieda a značkovanie v jednom .blade.php. Už žiadne skákanie medzi dvoma priečinkami kvôli komponentu na dvadsať riadkov.
Ostrovy (islands) Izolované oblasti vnútri komponentu, ktoré sa prekresľujú samostatne. Počítadlo tržieb už nespúšťa znova dotazy celého dashboardu.
Sloty Rodič vkladá značkovanie do potomka a to sa vyhodnocuje v kontexte rodiča: wire:click v slote volá metódu rodiča.
Optimistické UI wire:show, wire:text, wire:bind menia DOM okamžite, bez volania servera.
Drag and drop wire:sort je vstavaný. Žiadny SortableJS, žiadny lepiaci kód.

Je to prepísanie aplikácie?

Nie. Livewire 4 si zachováva vysokú spätnú kompatibilitu: triedne komponenty fungujú ďalej. Jednosúborový formát je predvolený pre nové komponenty, nie nútená migrácia. Existuje sada nekompatibilných zmien, ktoré si oplatí prečítať pred upgradom — sú zhrnuté v porovnaní Livewire 3 a 4 a oficiálny upgrade guide zostáva primárnym zdrojom.

Prichádzate z Livewire 3? Skutočná novinka je v kapitolách 3, 6, 10 a 11: jednosúborové komponenty, nová sémantika wire:model, ostrovy a sloty. Zvyšok modelu komponentu bude pôsobiť známo.

2. Inštalácia a požiadavky

Bash
composer require livewire/livewire:^4.0

php artisan optimize:clear

Ako v tretej verzii sa assety vkladajú automaticky. @livewireStyles a @livewireScripts treba umiestniť ručne len pri neštandardnom layoute alebo prísnej Content Security Policy.

Endpointy Livewire sa zmenili. URL aktualizácií teraz obsahujú hash: /livewire/ sa stalo /livewire-{hash}/. Ak máte pravidlá firewallu, výnimky WAF, obchádzky CDN alebo location bloky v nginxe naviazané na doslovnú cestu /livewire/, treba ich rozšíriť na nový tvar — inak bude aplikácia v produkcii vyzerať pokazene, hoci na notebooku funguje bezchybne.

Publikovanie konfigurácie

Bash
php artisan livewire:publish --config
NastavenieÚčel
component_locationsPriečinky prehľadávané kvôli komponentom. Predvolene resources/views/components a resources/views/livewire.
component_namespacesPomenované korene, napríklad pages::.
component_layoutV3 to bolo layout. Používa namespace layouts::.
component_placeholderV3 to bolo lazy_placeholder.
make_commandČo generuje make:livewire: jednosúborový alebo triedny komponent.
smart_wire_keysTeraz predvolene true.
csp_safeBuild kompatibilný s prísnejšou CSP.
config/livewire.php
// Naďalej generovať triedne komponenty namiesto jednosúborových
'make_command' => [
    'type' => 'class',
],

3. Jednosúborové komponenty

Hlavná zmena v každodennej práci. Komponent je teraz jeden Blade súbor, ktorý začína PHP blokom s anonymnou triedou a pokračuje značkovaním.

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

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

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

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

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

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

        $this->reset();

        $this->dispatch('notify', message: 'Príspevok bol vytvorený');
    }
};

?>

<div>
    <form wire:submit="save">
        <input type="text" wire:model="title" placeholder="Nadpis">
        @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">Uložiť</button>
    </form>
</div>

Verejné vlastnosti sú v značkovaní dostupné ako bežné premenné — {{ $title }} funguje rovnako ako v samostatnom súbore šablóny.

O tom emoji

Predvolený názov súboru nesie predponu . Je to vizuálny znak, ktorý odlišuje Livewire komponenty od bežných Blade komponentov v tom istom priečinku, a je voliteľný — v config/livewire.php sa dá vypnúť.

Viacsúborové komponenty

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

Triedne komponenty fungujú ďalej

app/Livewire/CreatePost.php
<?php

namespace App\Livewire;

use Livewire\Component;

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

    public function render()
    {
        return view('livewire.create-post');
    }
}
Čo si vybrať. Jednosúborový formát vyhráva pri malých a stredných komponentoch — formulár, riadok tabuľky, widget. Triedny alebo viacsúborový je lepší tam, kde trieda nesie skutočnú logiku a veľa závislostí.

4. Umiestnenie, pomenovanie a namespace

Štruktúra priečinkov
resources/views/
├── components/
│   ├── ⚡counter.blade.php              → <livewire:counter />
│   ├── post/
│   │   ├── ⚡create.blade.php           → <livewire:post.create />
│   │   └── ⚡index.blade.php            → <livewire:post.index />
│   └── button.blade.php                 (bežný Blade komponent)
└── pages/
    └── ⚡dashboard.blade.php            → <livewire:pages::dashboard />
Blade
{{-- Podľa mena --}}
<livewire:counter />

{{-- Vnorené priečinky bodkou --}}
<livewire:post.create />

{{-- S namespace --}}
<livewire:pages::post.create />

{{-- S parametrami --}}
<livewire:post.create :title="$initialTitle" :author="$user" />
Tagy komponentov musia byť uzavreté. Vo v4 sa neuzavretý <livewire:some-component> jednoducho nevykreslí — bez chyby, bez výstupu. Vždy samouzatvárajte: <livewire:some-component />. Toto je najčastejšie prekvapenie pri prenose Blade súborov z v3.

Smerovanie na komponent

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

// Podľa triedy
Route::livewire('/dashboard', Dashboard::class);

// Podľa mena komponentu
Route::livewire('/dashboard', 'pages::dashboard')
    ->middleware('auth')
    ->name('dashboard');

Layout

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

new class extends Component {
    #[Layout('layouts::app')]
    #[Title('Ovládací panel')]
    public function render() { /* … */ }
};

5. Vlastnosti a props

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

Props od rodiča: #[Prop]

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

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

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

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

?>

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

    @if ($dismissible)
        <button wire:click="$dispatch('dismiss')">×</button>
    @endif
</div>
Blade
<livewire:alert type="warning" dismissible class="mt-4">
    Faktúra je po splatnosti.
</livewire:alert>

mount() beží stále ako prvý

PHP
use App\Models\Order;

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

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

#[Locked] je rovnako dôležitý

PHP
use Livewire\Attributes\Locked;

new class extends Component {
    #[Locked]
    public int $invoiceId;   // klient nepodstrčí cudzie id

    public string $note = '';
};
Štvrtá verzia nemení nič na modeli hrozieb: každá verejná vlastnosť je zapisovateľná klientom, kým ju nezamknete, a každá verejná metóda je dosiahnuteľný endpoint. Pozri kapitolu 25.

6. wire:model vo štvrtej verzii

Táto zmena vás podrazí s najväčšou pravdepodobnosťou. Dva aspekty wire:model sa správajú inak a oba zlyhávajú ticho, bez chyby.

Zmena 1: modifikátory riadia synchronizáciu na klientovi

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

{{-- Ekvivalent rovnakého správania vo v4 --}}
<input wire:model.live.blur="title">

Zmena 2: udalosti už nebublajú od potomkov

Blade
{{-- Livewire 3: obal zachytil udalosť poľa --}}
<div wire:model="value">
    <input type="text">
</div>

{{-- Livewire 4: zapneme výslovne --}}
<div wire:model.deep="value">
    <input type="text">
</div>

Zvyšok bez zmeny

Blade
{{-- Predvolene odložené --}}
<input type="text" wire:model="name">

{{-- Požiadavka pri každej zmene --}}
<input type="text" wire:model.live="search">

{{-- Živé vyhľadávanie s oneskorením --}}
<input type="text" wire:model.live.debounce.400ms="search">

{{-- Vnorené dáta --}}
<input wire:model="form.address.city">
<input wire:model="post.title">
Väzba na vlastnosť modelu (post.title) stále vyžaduje zodpovedajúce validačné pravidlo. Je to ochrana pred hromadným priradením a do v4 prešla nezmenená.

7. Akcie, async a renderless

Blade
<button wire:click="save">Uložiť</button>

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

<button wire:click="delete({{ $post->id }})"
        wire:confirm="Zmazať príspevok? Akciu nemožno vrátiť.">Zmazať</button>

Akcie bez prekreslenia

PHP
use Livewire\Attributes\Renderless;

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

Asynchrónne akcie

#[Async] (alebo modifikátor .async) vykoná akciu paralelne, mimo bežného frontu požiadaviek. Ideálne pre „vystrelil a zabudol“: analytika, logovanie, zahrievanie cache.

PHP
use Livewire\Attributes\Async;

new class extends Component {
    #[Async]
    public function logInteraction(string $element): void
    {
        Analytics::record($element, auth()->id());
    }
};
Blade
<button wire:click.async="logInteraction('cta-hero')">Začať</button>
Nikdy nepoužívajte async pre akcie, ktoré menia stav zobrazený v UI. Keďže obchádzajú front požiadaviek, výsledok môže doraziť mimo poradia a dostanete stav, ktorý si protirečí s obrazovkou. Async je pre vedľajšie efekty, ktoré rozhranie nečíta späť.
Blade
<button wire:click="$refresh">Obnoviť</button>
<button wire:click="$set('tab', 'settings')">Nastavenia</button>
<button wire:click="$toggle('showFilters')">Filtre</button>
<button wire:click="$parent.closeModal()">Zavrieť</button>

8. Životný cyklus a hooky

Prvé vykreslenie
1. Vytvorí sa inštancia komponentu
2. boot()
3. mount($params)
4. booted()
5. render()
Následná aktualizácia
 1. Príde POST /livewire-{hash}/update so snapshotom stavu
 2. Overí sa kontrolný súčet snapshotu
 3. Vytvorí sa inštancia komponentu
 4. boot()
 5. Stav sa obnoví zo snapshotu
 6. hydrate() / hydrateFoo()
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)
 9. Vlastnosti dostanú nové hodnoty
10. updated($prop, $value) / updatedFoo($value)
11. Vykonajú sa akcie z frontu
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — stav sa zabalí späť do snapshotu
14. Odpoveď: značkovanie + effects
PHP
new class extends Component {
    public float $price = 0;

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

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

    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }
};
Ostrovy menia rozsah vykresľovania, nie poradie týchto hookov. Aktualizácia ostrova stále prebehne celým serverovým cyklom — len vráti fragment namiesto celého komponentu.

9. Validácia a Form objekty

PHP
use Livewire\Attributes\Validate;

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

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

    #[Validate('accepted', message: 'Je potrebný súhlas so spracovaním údajov.')]
    public bool $consent = false;

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

        Contact::create($data);

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

Form objekty

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

namespace App\Livewire\Forms;

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

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

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

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

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

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

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

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

Chyby v JavaScripte

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

10. Ostrovy (islands)

Ostrovy sú vlajkovou novinkou Livewire 4 a tou, ktorá mení uvažovanie o veľkosti komponentu. Ostrov je oblasť vnútri komponentu, ktorá sa prekresľuje nezávisle: pri aktualizácii sa prepočíta a zaplátkuje len tento fragment, nie celý komponent.

Blade
@island
    <div>Tržby: {{ $this->revenue }}</div>
@endisland

Prečo na tom záleží

Vo v3 bol štandardným liekom na „dashboard je pomalý“ rozpad na šesť podkomponentov, aby aktualizácia jedného widgetu nespúšťala znova dotazy ostatných piatich. Ostrovy dajú rovnakú izoláciu bez hranice komponentu: jeden komponent, jedna trieda, viacero nezávisle sa aktualizujúcich oblastí.

Dashboard s tromi ostrovmi
<?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>Tržby za mesiac</h3>
            <p class="stat">{{ number_format($this->revenue, 2, ',', ' ') }} €</p>
        </div>
    @endisland

    @island(name: 'queue', poll: '5s')
        <div class="card">
            <h3>Úloh vo fronte</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>

Možnosti

MožnosťÚčinok
nameMeno ostrova, aby ho mohli zacieliť akcie a JavaScript. Viac ostrovov s rovnakým menom sa vždy vykresľuje ako skupina.
lazyVykreslí sa, keď sa dostane do viewportu.
deferVykreslí sa hneď po načítaní stránky, bez ohľadu na viditeľnosť.
alwaysVynúti aktualizáciu pri každom vykreslení rodiča.
skipPreskočí prvotné vykreslenie.

Zacielenie ostrova z akcie

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

<button wire:click="$refresh" wire:island="revenue">Obnoviť tržby</button>

Append a prepend — nekonečný zoznam bez inštalatérstva

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

<button wire:click="loadMore" wire:island.append="feed">Načítať ďalšie</button>
Blade
<button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
    Načítať ďalšie
</button>

Polling v rámci ostrova

Blade
@island(name: 'queue')
    <div wire:poll.3s>
        Úloh vo fronte: {{ $this->queue }}
    </div>
@endisland
Ostrov alebo podkomponent. Ostrov vtedy, keď oblasť zdieľa stav rodiča a potrebuje len izolované vykresľovanie. Podkomponent vtedy, keď oblasť potrebuje vlastný stav, vlastný životný cyklus alebo sa používa na viacerých miestach.

11. Sloty a preposielanie atribútov

Livewire komponenty teraz prijímajú obsah slotu tak, ako to Blade komponenty vždy vedeli — s jednou dôležitou zvláštnosťou: obsah slotu sa vyhodnocuje v kontexte rodiča. wire:click napísaný v slote volá metódu rodiča, nie potomka.

Predvolený slot

Rodičovská šablóna
<livewire:modal>
    <h2>Vytvoriť príspevok</h2>

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

use Livewire\Component;

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

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

?>

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

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

Pomenované sloty

Rodičovská šablóna
<livewire:modal>
    <wire:slot name="header">
        <h2>Vytvoriť príspevok</h2>
    </wire:slot>

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

    <wire:slot name="footer">
        <button wire:click="save">Uložiť</button>
    </wire:slot>
</livewire:modal>
Vnútri komponentu
<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>

Preposielanie atribútov

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

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

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

?>

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $slot }}
</div>
Blade
<livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
    Faktúra je 14 dní po splatnosti.
</livewire:alert>

12. Vnorené komponenty a wire:key

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

Inteligentné kľúče

Vo štvrtej verzii je smart_wire_keys zapnuté predvolene, takže Livewire odvodí kľúče sám v mnohých situáciách s cyklami, kde predtým bol potrebný výslovný.

Kľúče píšte ďalej tam, kde záleží na identite. Každý zoznam, ktorý sa dá preusporiadať, filtrovať alebo čiastočne mazať, musí niesť výslovný wire:key naviazaný na id záznamu.
Blade
@foreach ($rows as $row)
    <div wire:key="row-{{ $row->id }}">…</div>
@endforeach
PHP
use Livewire\Attributes\Reactive;

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

new class extends Component {
    #[Modelable]
    public int $value = 0;
};
S ostrovmi siahnete po podkomponente menej často. Ak bol jediným dôvodom rozdelenia „aby sa to prekresľovalo samostatne“, ostrov je ľahší.

13. Udalosti

API udalostí sa oproti tretej verzii nezmenilo.

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

new class extends Component {
    #[On('post-created')]
    public function onPostCreated(int $postId, string $title): void
    {
        // …
    }
};
PHP
$this->dispatch('notify', type: 'success', message: 'Objednávka bola uložená');
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>
Ostrovy často nahrádzajú udalosti. Klasický vzor v3 „komponent A odošle, komponent B počúva a obnoví sa“ sa nezriedka mení na „obe oblasti sú ostrovy jedného komponentu a akcia cieli na susedný ostrov“ — jedna požiadavka namiesto dvoch.

14. Vypočítané vlastnosti

PHP
use Livewire\Attributes\Computed;

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

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

    #[Computed]
    public function subtotal(): float
    {
        return $this->products->sum(
            fn ($product) => $product->price * $this->items[$product->id]
        );
    }
};
Blade
@island(name: 'totals')
    <p>Medzisúčet: {{ number_format($this->subtotal, 2, ',', ' ') }} €</p>
@endisland
PHP
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
    return [
        'orders'  => Order::whereMonth('created_at', now()->month)->count(),
        'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
    ];
}
V šablóne je vypočítaná vlastnosť $this->products, nikdy nie $products.

15. Stav v URL a v relácii

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

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

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

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

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

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

    session()->flash('status', 'Nastavenia boli uložené');
}
Každá #[Url] vlastnosť volá pri zmene History API. Pri poliach meniacich sa s každým stlačením klávesy kombinujte s wire:model.live.debounce.

16. Nahrávanie súborov

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

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

new class extends Component {
    use WithFileUploads;

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

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

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

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

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

?>

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

        <div wire:loading wire:target="avatar">Nahrávame…</div>

        @if ($avatar)
            <img src="{{ $avatar->temporaryUrl() }}" alt="Náhľad" class="preview">
        @endif

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

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

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

    <div x-show="uploading" class="progress">
        <div class="progress__bar" :style="`width: ${progress}%`"></div>
    </div>
</div>
upload_max_filesize, post_max_size v PHP a client_max_body_size v nginxe musia prevyšovať vaše pravidlo max:.

17. Stránkovanie

PHP
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';

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

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

Ostrovy menia stránkovanie na nekonečný zoznam

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

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

    <button wire:click="nextPage" wire:island.append="rows">Načítať ďalšie</button>
</div>
Na veľkých tabuľkách stojí paginate() dotaz COUNT(*) navyše. simplePaginate() je citeľne lacnejší.

18. Stavy načítavania

Blade
<div wire:loading>Načítava sa…</div>
<div wire:loading.remove>Obsah</div>

<button wire:click="save">Uložiť</button>
<span wire:loading wire:target="save">Ukladáme…</span>

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

<div wire:loading.delay>Načítava sa…</div>
<div wire:loading.delay.long>Načítava sa…</div>

Štýlovanie z CSS

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

<span wire:dirty wire:target="title">Máte neuložené zmeny</span>

<div wire:offline class="banner banner--warn">
    Nie ste pripojení k serveru.
</div>

19. Optimistické UI na klientovi

Celá trieda interakcií server nikdy nepotrebovala: otvoriť panel, ukázať počítadlo znakov, zafarbiť pole načerveno. Štvrtá verzia im dáva plnohodnotné direktívy, ktoré menia DOM okamžite, úplne bez požiadavky.

Blade
{{-- Prepína viditeľnosť cez CSS, okamžite --}}
<div wire:show="showModal" class="modal">…</div>

<button wire:click="$toggle('showModal')">Otvoriť</button>
Blade
{{-- Text nasleduje vlastnosť na klientovi --}}
<span wire:text="title"></span>

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

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

<span wire:bind:class="message.length > 240 && 'text-red-500'">
    Začína byť dlhé
</span>
Optimistické neznamená smerodajné. Tieto direktívy menia to, čo používateľ vidí, skôr než server súhlasil. Všetko, čo ovplyvňuje dáta, oprávnenia alebo peniaze, sa aj tak musí validovať a aplikovať na serveri.

20. Drag and drop

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

Úchyty na ťahanie

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

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

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

Ťahanie medzi zoznamami

Blade
<div class="board">
    @foreach ($columns as $column)
        <ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
            @foreach ($column->cards as $card)
                <li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
                    {{ $card->title }}
                </li>
            @endforeach
        </ul>
    @endforeach
</div>
Poradie treba uložiť. Direktíva preusporiada DOM optimisticky a odovzdá vám novú postupnosť — zapísať ju do databázy je na vás.

21. Scoped CSS a skripty

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

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

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

?>

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

<style>
    /* Scoped na tento komponent — žiadna globálna kolízia na .title */
    .card  { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
    .title { font-size: 20px; font-weight: 700; }
    .price { color: #2d6a4f; }
</style>
Blade
<style global>
    :root { --brand: #f53004; }
</style>
Blade
@script
<script>
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Scoped CSS je pohodlie pre lokálne štýlovanie komponentu, nie náhrada dizajnového systému.

23. Integrácia s JavaScriptom

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

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

        <button x-on:click="$wire.save()">Uložiť</button>
        <button x-on:click="await $wire.calculate(); open = false">Prepočítať</button>
    </div>
</div>

wire:ref

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

<button x-on:click="$refs.modal.open()">Otvoriť modal</button>

#[Json]

PHP
use Livewire\Attributes\Json;

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

Interceptory

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

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

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

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

24. Testovanie

Pest je odporúčaný spôsob testovania komponentov Livewire 4.

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

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

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

it('uloží správny príspevok', function () {
    Livewire::actingAs($user)
        ->test('post.create')
        ->set('title', 'Celkom rozumný nadpis')
        ->set('body', str_repeat('Dostatočne dlhý text príspevku. ', 3))
        ->call('save')
        ->assertHasNoErrors()
        ->assertDispatched('notify');
});
MetódaOveruje
assertSet() / assertNotSet()Hodnoty vlastností
assertSee() / assertDontSee()Výstup
assertHasErrors() / assertHasNoErrors()Validáciu
assertDispatched()Udalosti
assertRedirect()Presmerovania
assertForbidden()Autorizáciu
Testujte prednostne autorizáciu v akciách, validačné pravidlá a prechody stavov.

25. Bezpečnosť a výkon

Model hrozieb je zhodný s v3. Každá verejná metóda je otvorený HTTP endpoint, každá verejná vlastnosť je zapisovateľná klientom.

Kontrolný zoznam bezpečnosti

  • $this->authorize() v každej akcii, ktorá mení dáta.
  • Všetky identifikátory označené #[Locked].
  • Pomocné metódy deklarované ako protected alebo private.
  • Validačné pravidlá neobsahujú služobné polia (user_id, role, price).
  • Žiadne tokeny ani kľúče vo verejných vlastnostiach — snapshot ide do prehliadača v čitateľnej podobe.
  • Nahrávanie súborov obmedzené podľa MIME typu a veľkosti.
  • Prihlasovacie formuláre za rate limiterom.
  • Optimistické UI sa nikdy nepovažuje za smerodajné.

Výkon

PríznakRiešenie vo v4
Aktualizácia widgetu spustí znova všetky dotazy stránkyZabaliť oblasti do @island
Obrovské HTML stránkyDostať kolekcie modelov z verejných vlastností
Požiadavka pri každom stlačení klávesywire:model.live.debounce.400ms
Záťaž z pollinguPolling vnútri ostrova plus .visible
Round trip kvôli prepnutiu paneluwire:show namiesto akcie
Inkrement počítadla prekresľuje stránku#[Renderless]
Dotazy N+1Eager loading cez with()
app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

26. Časté chyby a ťahák

Komponent sa vôbec nevykreslí

Tag nie je samouzatvorený. Píšte <livewire:my-component />.

V produkcii všetko 404, lokálne funguje

Endpoint Livewire sa presunul z /livewire/ na /livewire-{hash}/. Pravidlo firewallu, výnimka WAF alebo location v nginxe blokujú aktualizačné požiadavky.

Pole prestalo synchronizovať po upgrade

Pridajte .live: wire:model.live.blur="title".

Obal prestal zachytávať svoje pole

Použite wire:model.deep.

Modifikátory wire:transition nič nerobia

Boli odstránené — v4 používa natívne View Transitions API. Animáciu vyjadrite v CSS.

Ostrov sa neaktualizuje

  • Ostrov nemá name, ale akcia cieli na meno.
  • wire:island="…" ukazuje na meno, ktoré vo výstupe neexistuje.
  • Oblasť závisí od stavu rodiča, ktorý sa reálne nezmenil — pridajte always: true.

Komponenty Volt prestali fungovať

Volt sa stal súčasťou jadra. Nahraďte Livewire\Volt\Component za Livewire\Component, Volt::route() za Route::livewire(), odstráňte service provider a balík.

Ťahák direktív

DirektívaÚčelNové vo v4
wire:modelVäzba poľa na vlastnosťzmenená sémantika
wire:model.deepZachytiť udalosti z potomkováno
wire:click.asyncVykonať akciu paralelneáno
wire:click.renderlessPreskočiť prekreslenieáno
wire:islandZacieliť ostrováno
wire:island.appendPridať do ostrovaáno
wire:showPrepnúť viditeľnosť na klientoviáno
wire:textNaviazať text na klientoviáno
wire:bindReaktívne naviazať atribútáno
wire:sortDrag and dropáno
wire:intersectAkcia pri vstupe do viewportuáno
wire:refMeno prvku pre JSáno
wire:navigate:scrollZachovať scroll kontajnerapremenované
wire:pollPeriodická aktualizáciav rámci ostrova
wire:keyIdentita prvku v cyklesmart keys predvolene

Blade direktívy

DirektívaÚčel
@island … @endislandNezávisle vykresľovaná oblasť
@placeholder … @endplaceholderZástupný obsah pre lenivý ostrov
<wire:slot name="…">Obsah pomenovaného slotu
@script … @endscriptJavaScript v rámci komponentu
@assets … @endassetsAssety stránky načítané raz
<style> / <style global>Scoped / globálne CSS

PHP atribúty

AtribútÚčel
#[Validate] / #[Locked]Validácia a ochrana pred zápisom
#[Computed]Cachovaná odvodená hodnota
#[Url] / #[Session]Uchovanie stavu
#[On] / #[Prop]Udalosti a props
#[Async] / #[Renderless]Paralelnosť a preskočenie renderu
#[Json]Dáta priamo do JavaScriptu
Bash
php artisan make:livewire post.create              # jednosúborový
php artisan make:livewire post.create --mfc        # viacsúborový
php artisan make:livewire post.create --test       # s testom
php artisan livewire:publish --config
php artisan optimize:clear

Oficiálne zdroje

Livewire 4 sa aktívne vyvíja. Ak sa tento sprievodca a oficiálna dokumentácia rozchádzajú, platí oficiálny zdroj.