Livewire 3 — повний посібник

Livewire дозволяє будувати динамічні інтерфейси в Laravel на PHP і Blade — без окремого фронтенд-застосунку та без API-шару. Цей посібник веде від встановлення до продакшену: компоненти, стан, форми, події, завантаження файлів, тестування, безпека та продуктивність.

Livewire 3.x Laravel 10 / 11 / 12 PHP 8.1+ 24 розділи

1. Що таке Livewire

Livewire — це повнофункціональний фреймворк для Laravel, який дозволяє будувати динамічні інтерфейси на PHP і Blade, не виходячи з бекенду. Ви не пишете окремий SPA, не піднімаєте REST/GraphQL-шар і не тримаєте другу модель даних у JavaScript.

Як це працює

Компонент Livewire — це PHP-клас і пов'язаний з ним Blade-шаблон. Під час першого завантаження сторінки компонент рендериться на сервері як звичайний HTML. Далі невеликий JS-рантайм Livewire перехоплює дії користувача (клік, введення, submit), надсилає AJAX-запит на сервер із поточним станом компонента, сервер заново рендерить компонент і повертає новий HTML. Рантайм порівнює старий і новий DOM та точково латає відмінності — сторінка не перезавантажується.

Схема одного циклу
Браузер                          Сервер
   │                                │
   │  клік по wire:click="save"     │
   ├───────── POST /livewire/update ┤
   │  { snapshot, calls, updates }  │
   │                                ├── відновити компонент зі snapshot
   │                                ├── застосувати оновлення властивостей
   │                                ├── викликати метод save()
   │                                ├── виконати render()
   │  { snapshot, html, effects }   │
   ├◄───────────────────────────────┤
   ├── morph DOM (лише відмінності) │
   ▼                                ▼

Коли Livewire — правильний вибір

  • Адмінки, CRM, дашборди, внутрішні панелі — багато форм і таблиць, мало складної анімації.
  • Форми із залежними полями, багатокрокові візарди, жива валідація.
  • Таблиці з пошуком, фільтрами, сортуванням і пагінацією.
  • Команда сильна в PHP і не хоче утримувати окремий фронтенд-стек.
  • Потрібен швидкий випуск функціональності без дублювання логіки між PHP і JS.

Коли краще взяти щось інше

  • Інтерфейси з високою частотою оновлень: онлайн-редактори, канвас, drag-and-drop-конструктори, ігри.
  • Offline-first застосунки та PWA зі складною локальною синхронізацією.
  • Мобільні застосунки — там потрібен API, а не HTML-over-the-wire.
  • Поганий або високолатентний канал: кожен цикл — це мережевий round-trip.
Правило великого пальця: якщо взаємодії потрібні дані з сервера — це Livewire. Якщо взаємодія суто візуальна (відкрити меню, перемкнути таб, показати тултип) — це Alpine.js на клієнті, без звернення до сервера.

Версії

Цей посібник описує Livewire 3.x. Вимоги: PHP 8.1+, Laravel 10 і вище. У третій версії Alpine.js входить у комплект, змінилися імена подій (dispatch замість emit), з'явилися PHP-атрибути (#[Computed], #[Validate], #[Url]), а wire:model за замовчуванням став відкладеним.

2. Встановлення та налаштування

Встановлення в наявний Laravel-проєкт займає одну команду.

Bash
composer require livewire/livewire

У Livewire 3 окремо підключати ассети не обов'язково: пакет сам вставляє @livewireStyles і @livewireScripts у layout. Але якщо ви використовуєте нестандартний layout або CSP, директиви можна прописати вручну.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? 'Застосунок' }}</title>

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

    {{ $slot }}

    @livewireScripts
</body>
</html>
Часта помилка. Якщо компонент рендериться, але не реагує на кліки — майже завжди в layout бракує @livewireScripts або він стоїть до підключення вашого власного Alpine.js. Livewire 3 уже містить Alpine — другий екземпляр Alpine ламає реактивність.

Публікація конфігурації

Bash
php artisan livewire:publish --config

Основні параметри config/livewire.php:

ПараметрПризначення
class_namespaceNamespace класів компонентів. За замовчуванням App\Livewire.
view_pathКаталог Blade-шаблонів компонентів.
layoutLayout для full-page компонентів.
temporary_file_uploadДиск, строк життя і правила для тимчасових завантажень.
inject_assetsАвтоматична вставка CSS/JS. Вимкніть, якщо ставите директиви вручну.
navigate.show_progress_barСмуга прогресу для wire:navigate.

Перевірка встановлення

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

3. Перший компонент

Компонент складається з двох файлів: класу та шаблону.

app/Livewire/Counter.php
<?php

namespace App\Livewire;

use Livewire\Component;

class Counter extends Component
{
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }

    public function decrement(): void
    {
        $this->count--;
    }

    public function reset(): void
    {
        $this->count = 0;
    }

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

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

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

    <button type="button" wire:click="reset" class="btn btn-ghost">Скинути</button>
</div>
Один кореневий елемент. Шаблон компонента зобов'язаний мати рівно один кореневий елемент HTML. Коментарі й текст на верхньому рівні теж ламають morph-алгоритм. Якщо потрібно кілька блоків — загорніть їх у <div>.

Три способи вивести компонент

Blade
{{-- 1. Тег-синтаксис (рекомендовано) --}}
<livewire:counter />

{{-- 2. Директива --}}
@livewire('counter')

{{-- 3. З параметрами --}}
<livewire:counter :start="10" title="Лічильник замовлень" />

Full-page компоненти

Компонент можна повісити прямо на маршрут — тоді він рендериться як самостійна сторінка.

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

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

class Dashboard extends Component
{
    #[Layout('layouts.app')]
    #[Title('Панель керування')]
    public function render()
    {
        return view('livewire.dashboard');
    }
}

Inline-компоненти

Для дуже маленьких компонентів шаблон можна повернути рядком — окремий Blade-файл не потрібен.

PHP
public function render()
{
    return <<<'BLADE'
        <div>
            <button wire:click="$refresh">Оновити</button>
        </div>
    BLADE;
}

4. Властивості компонента

Публічні властивості класу автоматично доступні в Blade-шаблоні та зберігаються між запитами. Саме вони утворюють стан компонента.

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

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

Які типи можна зберігати

Між запитами стан серіалізується в JSON, тому набір допустимих типів обмежений.

МожнаНе можна
string, int, float, bool, null Замикання (Closure)
array зі скалярних значень Ресурси (resource), потоки
Eloquent-моделі та Collection моделей Довільні об'єкти без підтримки Wireable/Synth
Carbon, DateTime, Stringable, enum Об'єкти з посиланнями на підключення, PDO тощо
Eloquent-моделі у властивостях. Модель зберігається як ідентифікатор і заново завантажується з бази на кожному запиті. Це зручно, але кожен цикл додає SQL-запит. Якщо потрібно лише кілька полів — зберігайте скаляри, а не модель цілком.

Ініціалізація: mount()

Метод mount() — конструктор компонента. Він виконується один раз, під час першого рендеру, і отримує параметри, передані з Blade.

PHP
use App\Models\Order;

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

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

Захист властивостей: #[Locked]

За замовчуванням клієнт може підмінити значення будь-якої публічної властивості. Для ідентифікаторів і всього, що впливає на права доступу, це діра. Атрибут #[Locked] забороняє зміну властивості з фронтенду.

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;   // клієнт не зможе підставити чужий id

    public string $comment = '';
}

Приховані від JSON властивості

Властивості з модифікаторами protected і private не серіалізуються та скидаються між запитами. Використовуйте їх лише для значень, які обчислюються заново в кожному циклі.

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

    // Скинеться після кожного запиту — не зберігайте тут стан.
    protected array $cache = [];
}

5. wire:model і прив'язка даних

wire:model пов'язує поле форми з властивістю компонента. У Livewire 3 прив'язка за замовчуванням відкладена: значення їде на сервер не на кожне натискання клавіші, а разом із наступною дією (клік, submit).

Blade
{{-- Відкладено: значення піде з наступним запитом --}}
<input type="text" wire:model="name">

{{-- Негайно: запит на кожну зміну --}}
<input type="text" wire:model.live="search">

{{-- Із затримкою 500 мс після завершення введення --}}
<input type="text" wire:model.live.debounce.500ms="search">

{{-- Не частіше разу на 2 с, поки користувач друкує --}}
<input type="text" wire:model.live.throttle.2s="search">

{{-- Лише при втраті фокусу --}}
<input type="text" wire:model.blur="email">

{{-- Властивість змінюється миттєво на клієнті, сервер дізнається пізніше --}}
<input type="text" wire:model.lazy="draft">

Модифікатори

МодифікаторПоведінкаКоли застосовувати
Відкладене надсиланняЗвичайні поля форми
.liveЗапит на кожну змінуЖивий пошук, залежні селекти
.blurЗапит при втраті фокусуВалідація поля на виході
.debounce.XmsЧекати паузу у введенніПошук під час набору
.throttle.XsНе частіше одного разу за інтервалВажкі запити
.numberПриводити до числаЧислові поля
.booleanПриводити до булевогоСелекти «так/ні»
.fillУзяти початкове значення з HTMLПопередньо заповнені форми

Усі типи полів

Blade
{{-- Текст, textarea --}}
<input type="text" wire:model="title">
<textarea wire:model="body"></textarea>

{{-- Один чекбокс: bool --}}
<input type="checkbox" wire:model="agreed">

{{-- Група чекбоксів: 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">

{{-- Радіокнопки --}}
<input type="radio" value="card" wire:model="payment">
<input type="radio" value="cash" wire:model="payment">

{{-- Select --}}
<select wire:model.live="categoryId">
    <option value="">Усі категорії</option>
    @foreach ($categories as $category)
        <option value="{{ $category->id }}">{{ $category->name }}</option>
    @endforeach
</select>

{{-- Множинний select --}}
<select wire:model="tagIds" multiple>
    @foreach ($tags as $tag)
        <option value="{{ $tag->id }}">{{ $tag->name }}</option>
    @endforeach
</select>

Вкладені дані

Крапкова нотація працює і для масивів, і для властивостей моделей.

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

public Post $post;
Blade
<input wire:model="form.name">
<input wire:model="form.address.city">
<input wire:model="post.title">
Прив'язка до моделі. Щоб wire:model="post.title" працювало, поле має бути дозволене правилом валідації (rules або #[Validate]) — інакше Livewire викине виняток. Це захист від масового присвоєння.

6. Дії (actions)

Дія — публічний метод компонента, який викликається з шаблону. Це заміна звичному fetch() + обробник на сервері.

Blade
{{-- Клік --}}
<button wire:click="save">Зберегти</button>

{{-- Надсилання форми (submit перехоплюється) --}}
<form wire:submit="save">
    <input wire:model="title">
    <button type="submit">Надіслати</button>
</form>

{{-- Клавіші --}}
<input wire:keydown.enter="search" wire:keydown.escape="clear">

{{-- Інші події DOM --}}
<div wire:mouseenter="preload">…</div>
<select wire:change="applyFilter">…</select>

Параметри

Blade
<button wire:click="delete({{ $post->id }})">Видалити</button>
<button wire:click="setStatus('published')">Опублікувати</button>
<button wire:click="move({{ $item->id }}, 'up')">Вгору</button>
PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);   // авторизацію перевіряємо завжди

    $post->delete();

    $this->dispatch('notify', message: 'Допис видалено');
}
Ніколи не довіряйте параметрам. Будь-який метод компонента доступний як публічний HTTP-ендпоінт. Клієнт може викликати delete(999) з довільним id. Перевірка прав усередині методу обов'язкова.

Model binding у параметрах

Livewire вміє розв'язувати моделі за id, як це робить роутер Laravel.

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

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

Модифікатори дій

Blade
{{-- Підтвердження у браузері до надсилання --}}
<button wire:click="delete" wire:confirm="Точно видалити? Скасувати буде неможливо.">Видалити</button>

{{-- preventDefault / stopPropagation --}}
<a href="#" wire:click.prevent="open">Відкрити</a>
<div wire:click.stop="select">…</div>

{{-- Спрацює один раз --}}
<button wire:click.once="init">Ініціалізувати</button>

{{-- Лише якщо клікнули саме по цьому елементу --}}
<div wire:click.self="close">…</div>

Магічні дії

ДіяЩо робить
$refreshПеререндерити компонент без зміни стану
$set('prop', value)Присвоїти властивості значення
$toggle('prop')Інвертувати булеву властивість
$dispatch('event')Надіслати подію Livewire
$parent.method()Викликати метод батьківського компонента
Blade
<button wire:click="$refresh">Оновити</button>
<button wire:click="$set('tab', 'settings')">Налаштування</button>
<button wire:click="$toggle('showFilters')">Фільтри</button>
<button wire:click="$parent.closeModal()">Закрити</button>

Редиректи

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

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

    session()->flash('status', 'Допис створено');

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

7. Життєвий цикл запиту

Розуміння порядку виконання знімає 90% «містичних» багів. Розрізняють первинний рендер (звичайний HTTP-запит сторінки) та подальші оновлення (AJAX-запити Livewire).

Первинний рендер

Порядок
1. Створюється екземпляр компонента
2. boot()
3. mount($params)
4. booted()
5. hydrate-хуки НЕ викликаються
6. render()
7. HTML вставляється у сторінку

Подальше оновлення

Порядок
 1. Надходить POST /livewire/update зі snapshot стану
 2. Перевіряється контрольна сума snapshot (захист від підміни)
 3. Створюється екземпляр компонента
 4. boot()
 5. Стан відновлюється зі snapshot
 6. hydrate() і hydrateFoo() для кожної властивості
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)   — до присвоєння
 9. Властивості отримують нові значення
10. updated($prop, $value) / updatedFoo($value)     — після присвоєння
11. Викликаються методи з черги calls (дії)
12. rendering()
13. render()
14. rendered($view, $html)
15. dehydrate() — стан пакується назад у snapshot
16. Відповідь: новий HTML + effects (події, редиректи, dispatch у браузер)

Snapshot і контрольна сума

Увесь стан компонента їде на клієнт і назад в атрибуті wire:snapshot. Щоб клієнт не підмінив дані, Livewire підписує snapshot HMAC-підписом на основі APP_KEY. За розбіжності підпису запит відхиляється з помилкою «Livewire encountered corrupt data».

Практичний наслідок. Що більше стану в публічних властивостях, то більше даних їздить мережею в кожному циклі. Колекція з 500 моделей у публічній властивості — це мегабайти трафіку на кожен клік. Тримайте у властивостях мінімум, а списки отримуйте в render() або через #[Computed].

Що відбувається з DOM

Livewire не замінює вузол цілком, а виконує morph: обходить старе й нове дерево і змінює лише відмінності. Тому фокус у полі введення, позиція скролу і стан Alpine зберігаються. Якщо структура списку змінюється, morph-алгоритму потрібні підказки — див. wire:key у розділі 10.

8. Хуки життєвого циклу

Хуки дозволяють втрутитися в будь-яку фазу циклу.

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

    // Виконується на початку КОЖНОГО запиту, до відновлення стану.
    public function boot(): void
    {
        // Гарне місце для залежностей, які не можна серіалізувати.
    }

    // Лише під час першого рендеру.
    public function mount(Product $product): void
    {
        $this->product = $product;
        $this->name    = $product->name;
        $this->price   = $product->price;
    }

    // Після відновлення стану, у кожному подальшому запиті.
    public function hydrate(): void
    {
    }

    // Після boot() і відновлення стану.
    public function booted(): void
    {
    }

    // Перед зміною будь-якої властивості.
    public function updating(string $property, mixed $value): void
    {
    }

    // Після зміни будь-якої властивості.
    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    // Лише для властивості $price.
    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }

    // Для вкладеного ключа 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
    {
    }

    // Перед пакуванням стану у snapshot.
    public function dehydrate(): void
    {
    }

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

Правила іменування

ВластивістьХук
$priceupdatedPrice()
$isActiveupdatedIsActive()
$form['email']updatedFormEmail()
$post->titleupdatedPostTitle()
Типовий прийом. Скидання пагінації при зміні фільтра робиться саме в хуку: public function updatedSearch() { $this->resetPage(); }

9. Валідація та Form-об'єкти

Livewire використовує валідатор Laravel — правила, повідомлення й локалізація ті самі.

Спосіб 1: атрибути (Livewire 3)

PHP
use Livewire\Attributes\Validate;

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

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

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

    #[Validate('accepted', message: 'Потрібна згода на обробку даних.')]
    public bool $consent = false;

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

        Contact::create($data);

        $this->reset();

        session()->flash('status', 'Повідомлення надіслано');
    }
}

Спосіб 2: метод rules()

Потрібен, коли правила залежать від стану.

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

protected function messages(): array
{
    return [
        'email.unique' => 'Така адреса вже зареєстрована.',
    ];
}

protected function validationAttributes(): array
{
    return [
        'email' => 'адреса електронної пошти',
    ];
}

Жива валідація

PHP
public function updated(string $property): void
{
    $this->validateOnly($property);   // перевіряємо лише змінене поле
}
Blade
<form wire:submit="submit" novalidate>
    <label for="email">E-mail</label>
    <input id="email" type="email" wire:model.blur="email"
           class="@error('email') border-red-500 @enderror">

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

    <button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove wire:target="submit">Надіслати</span>
        <span wire:loading wire:target="submit">Надсилаємо…</span>
    </button>
</form>

Ручне керування помилками

PHP
$this->addError('email', 'Домен у чорному списку.');
$this->resetValidation('email');
$this->resetValidation();               // скинути всі
$this->validateOnly('email');

// Проброс помилки як у звичайному Laravel-контролері
throw ValidationException::withMessages([
    'code' => 'Невірний код підтвердження.',
]);

Form-об'єкти

Коли форма розростається, виносьте її в окремий клас. Компонент лишається тонким, а правила й дані форми можна перевикористати між «створити» та «редагувати».

Bash
php artisan livewire:form PostForm
app/Livewire/Forms/PostForm.php
<?php

namespace App\Livewire\Forms;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    <button type="submit">Зберегти</button>
</form>

10. Вкладені компоненти та wire:key

Компоненти вкладаються один в одного. Кожен вкладений компонент — незалежна одиниця зі своїм станом і своїм циклом оновлення: клік усередині дочірнього компонента не перерендерює батьківський.

Blade
<div>
    <h1>Замовлення</h1>

    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>
:key обов'язковий у циклах. Без унікального ключа morph-алгоритм переплутає елементи під час сортування, фільтрації чи видалення: значення полів «переїдуть» на сусідні рядки. Ключ має бути стабільним — $loop->index не підходить, використовуйте id запису.

Передача даних униз

Параметри передаються один раз, у mount(). Під час подальших рендерів батьківського компонента дочірній не оновлюється автоматично — він живе своїм життям.

PHP
use Livewire\Attributes\Reactive;

class OrderTotal extends Component
{
    // З #[Reactive] значення приїжджатиме від батька під час кожного його рендеру.
    #[Reactive]
    public int $quantity;
}

Звернення до батька

Blade
<button wire:click="$parent.refreshList()">Оновити список</button>

Двобічний зв'язок: #[Modelable]

Дозволяє використовувати wire:model на самому компоненті — зручно для полів-віджетів.

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

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

    public function set(int $value): void
    {
        $this->value = $value;
    }
}
Blade
{{-- У батьківському шаблоні --}}
<livewire:rating-input wire:model.live="review.rating" />

Умовний рендер вкладених компонентів

Blade
@if ($showDetails)
    <livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif
Коли дробити на компоненти. Виділяйте вкладений компонент, якщо блок має власний стан або оновлюється незалежно (рядок таблиці, модальне вікно, віджет). Якщо блок — просто розмітка, використовуйте звичайний Blade-partial: він дешевший.

11. Події

Події — спосіб зв'язати компоненти, які не перебувають у відносинах «батько — нащадок». У Livewire 3 надсилання — це dispatch() (у версії 2 було emit()).

Надсилання

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();
Blade
{{-- Прямо з шаблону --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">Нове замовлення</button>

Приймання

PHP
use Livewire\Attributes\On;

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

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

    // Динамічне ім'я події
    #[On('order-updated.{orderId}')]
    public function onOrderUpdated(): void
    {
        $this->refreshList();
    }
}

Альтернатива — масив $listeners (сумісно з версією 2):

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

Події у браузер

Livewire може надіслати звичайну DOM-подію — її зловить Alpine.js або ваш власний JS. Це правильний спосіб показувати тости й відкривати модальні вікна.

PHP
$this->dispatch('notify', type: 'success', message: 'Замовлення збережено');
Blade
<div x-data="{ show: false, message: '' }"
     x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
     x-show="show"
     x-transition
     class="toast">
    <span x-text="message"></span>
</div>
JavaScript
document.addEventListener('notify', (event) => {
    console.log(event.detail.message);
});
Не зловживайте подіями. Подія — це зайвий цикл запиту для кожного компонента, що слухає. Якщо два блоки завжди змінюються разом, часто дешевше об'єднати їх в один компонент.

12. Обчислювані властивості

#[Computed] — спосіб отримувати похідні дані без зберігання їх у стані. Результат кешується на час одного запиту, тож звернення до нього тричі в шаблоні не дасть трьох SQL-запитів.

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

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

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

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

    #[Computed]
    public function total(): float
    {
        return round($this->subtotal * 1.2, 2);   // з ПДВ
    }
}
Blade
<div>
    @foreach ($this->products as $product)
        <div>{{ $product->name }} — {{ $this->items[$product->id] }} шт.</div>
    @endforeach

    <p>Сума: {{ number_format($this->subtotal, 2, ',', ' ') }} €</p>
    <p>Разом із ПДВ: {{ number_format($this->total, 2, ',', ' ') }} €</p>
</div>
Звернення через $this. У шаблоні обчислювана властивість доступна як $this->products, а не $products. Це відрізняє її від звичайних публічних властивостей.

Кешування між запитами

PHP
// Кеш на 5 хвилин у загальному кеші застосунку
#[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'),
    ];
}

// Спільний кеш для всіх користувачів
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
    return app(StatsService::class)->build();
}

Скидання кешу

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

    unset($this->products, $this->subtotal, $this->total);   // скинути кеш
}

Дані в render() vs #[Computed]

Обидва варіанти не зберігають дані у стані. Різниця в області видимості: render() віддає змінні лише шаблону, а #[Computed] доступне і в PHP-методах, і в шаблоні, та кешується.

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

13. Стан в URL і сесії

Фільтри, пошук і активна вкладка мають переживати перезавантаження сторінки та потрапляти в посилання, яким можна поділитися. Атрибут #[Url] синхронізує властивість із рядком запиту.

PHP
use Livewire\Attributes\Url;

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

    // Інше ім'я параметра в URL
    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    // Не показувати в URL, поки значення збігається з початковим
    #[Url(except: '')]
    public string $sort = 'popular';

    // Зберігати між переходами wire:navigate
    #[Url(keep: true)]
    public int $perPage = 24;

    // Використовувати history.pushState замість replaceState
    #[Url(history: true)]
    public string $tab = 'all';
}

Результат: /catalog?search=ноутбук&cat=5&sort=price-asc&tab=sale

Зберігання в сесії

PHP
use Livewire\Attributes\Session;

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

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

Ручна робота із сесією та flash

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

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

    session()->flash('status', 'Налаштування збережено');
}
Blade
@if (session('status'))
    <div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif
Не кладіть в URL усе поспіль. Кожна #[Url]-властивість при зміні смикає History API. Для полів, що змінюються на кожне натискання клавіші, комбінуйте з wire:model.live.debounce.

14. Завантаження файлів

Трейт WithFileUploads додає повноцінне завантаження: файл іде на сервер одразу після вибору, потрапляє у тимчасове сховище й стає доступним як об'єкт TemporaryUploadedFile.

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

class AvatarUploader extends Component
{
    use WithFileUploads;

    #[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]   // до 4 МБ
    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: 'Аватар оновлено');
    }

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

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

    {{-- Прогрес завантаження --}}
    <div wire:loading wire:target="avatar" class="text-sm">Завантажуємо файл…</div>

    {{-- Прев'ю до збереження --}}
    @if ($avatar)
        <img src="{{ $avatar->temporaryUrl() }}" alt="Прев'ю" 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">Зберегти</button>
</form>

Точний індикатор прогресу

Blade
<div x-data="{ progress: 0, uploading: false }"
     x-on:livewire-upload-start="uploading = true"
     x-on:livewire-upload-finish="uploading = false; progress = 0"
     x-on:livewire-upload-error="uploading = false"
     x-on:livewire-upload-progress="progress = $event.detail.progress">

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

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

Налаштування тимчасового сховища

config/livewire.php
'temporary_file_upload' => [
    'disk'       => 's3',            // або null → диск за замовчуванням
    'rules'      => ['file', 'max:12288'],
    'directory'  => 'livewire-tmp',
    'middleware' => 'throttle:60,1',
    'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
    'max_upload_time' => 5,          // хвилин до автоочищення
],
Перевірте ліміти PHP. upload_max_filesize, post_max_size і max_execution_time в php.ini, а також client_max_body_size у nginx мають бути не меншими за ваш max: у правилах. Інакше завантаження обривається без зрозумілої помилки.

15. Пагінація

Трейт WithPagination підключає пагінацію Laravel без перезавантаження сторінки.

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

class OrderTable extends Component
{
    use WithPagination;

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

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

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

    // Скидаємо на першу сторінку при зміні фільтрів.
    public function updatedSearch(): void
    {
        $this->resetPage();
    }

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

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

        $this->resetPage();
    }

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

    <table>
        <thead>
            <tr>
                <th wire:click="sortBy('number')" style="cursor:pointer">Номер</th>
                <th wire:click="sortBy('created_at')" style="cursor:pointer">Дата</th>
                <th wire:click="sortBy('total')" style="cursor:pointer">Сума</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">Замовлень не знайдено</td></tr>
            @endforelse
        </tbody>
    </table>

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

Корисні деталі

PHP
// Власне ім'я параметра сторінки — потрібне, якщо на сторінці два пагінатори
protected string $paginationTheme = 'tailwind';   // або 'bootstrap'

public function render()
{
    return view('livewire.dashboard', [
        'orders'   => Order::paginate(10, pageName: 'orders-page'),
        'invoices' => Invoice::paginate(10, pageName: 'invoices-page'),
    ]);
}
Продуктивність. На великих таблицях paginate() виконує додатковий COUNT(*). Якщо загальна кількість сторінок не потрібна, використовуйте simplePaginate() — він помітно дешевший.

16. Стани завантаження

Кожна дія — це мережевий запит. Без індикації інтерфейс здається завислим. Livewire дає декларативні директиви, які не потребують жодного рядка JS.

Blade
{{-- Показати під час будь-якого запиту компонента --}}
<div wire:loading>Завантаження…</div>

{{-- Приховати під час запиту --}}
<div wire:loading.remove>Вміст</div>

{{-- Лише для конкретної дії --}}
<button wire:click="save">Зберегти</button>
<span wire:loading wire:target="save">Зберігаємо…</span>

{{-- Для кількох цілей --}}
<span wire:loading wire:target="save,delete,publish">Обробляємо…</span>

{{-- Для конкретної властивості --}}
<span wire:loading wire:target="search">Шукаємо…</span>

{{-- Виключити ціль --}}
<div wire:loading wire:target.except="search">Оновлюємо…</div>

Модифікатори

Blade
{{-- Заблокувати кнопку --}}
<button wire:click="save" wire:loading.attr="disabled">Зберегти</button>

{{-- Додати CSS-клас --}}
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Зберегти</button>

{{-- Прибрати клас --}}
<div wire:loading.class.remove="bg-white">…</div>

{{-- Затримка: показати індикатор, лише якщо запит довший за 300 мс --}}
<div wire:loading.delay>Завантаження…</div>

{{-- Точні пороги: shortest 50ms, shorter 100ms, short 150ms,
     default 200ms, long 300ms, longer 500ms, longest 1s --}}
<div wire:loading.delay.long>Завантаження…</div>

{{-- Керування відображенням --}}
<div wire:loading.flex>…</div>
<div wire:loading.grid>…</div>
<div wire:loading.inline-flex>…</div>

Незбережені зміни: wire:dirty

Blade
<input wire:model="title">

<span wire:dirty wire:target="title" class="text-amber-600">Є незбережені зміни</span>

<button wire:click="save" wire:dirty.class="ring-2 ring-amber-400">Зберегти</button>

Втрата з'єднання: wire:offline

Blade
<div wire:offline class="banner banner--warn">
    Немає з'єднання із сервером. Зміни не зберігаються.
</div>

Скелетон під час першого завантаження

Blade
<div wire:init="loadHeavyData">
    @if ($loaded)
        {{-- реальні дані --}}
    @else
        <div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
    @endif
</div>

18. Ліниве завантаження та polling

Ліниве завантаження компонента

Важкий компонент можна не рендерити в першій відповіді: сторінка віддається миттєво із заглушкою, а вміст підвантажується другим запитом.

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

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

    public function render()
    {
        return view('livewire.revenue-chart', [
            'points' => app(RevenueService::class)->monthly(),   // важкий запит
        ]);
    }
}

За замовчуванням лінивий компонент вантажиться одразу після відмальовування сторінки. Варіант #[Lazy(isolate: false)] об'єднує запити кількох лінивих компонентів в один, а lazy="on-load" / lazy="on-scroll" керують моментом завантаження.

Blade
{{-- Підвантажити, коли блок потрапить у в'юпорт --}}
<livewire:revenue-chart lazy="on-scroll" />

Відкладена ініціалізація: wire:init

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

Опитування сервера: wire:poll

Blade
{{-- Оновлювати кожні 2 с (значення за замовчуванням — 2500 мс) --}}
<div wire:poll>…</div>

{{-- Власний інтервал --}}
<div wire:poll.5s>…</div>
<div wire:poll.750ms>…</div>

{{-- Викликати конкретний метод --}}
<div wire:poll.10s="refreshQueue">…</div>

{{-- Зупиняти опитування, коли вкладка неактивна --}}
<div wire:poll.visible.5s="refreshQueue">…</div>

{{-- Зупиняти через 5 хвилин неактивності користувача --}}
<div wire:poll.keep-alive.5s>…</div>
Вартість опитування. wire:poll.2s на сторінці, відкритій у 100 співробітників, — це 3000 запитів за хвилину до вашого PHP-процесу. Завжди додавайте .visible, збільшуйте інтервал до розумного і розглядайте websockets (Laravel Echo + Reverb) для по-справжньому живих даних.

19. Alpine.js, $wire і JS-хуки

Alpine.js входить у постачання Livewire 3. Усередині компонента доступний об'єкт $wire — проксі до стану й методів PHP-компонента прямо з JavaScript.

Blade
<div x-data="{ open: false }">
    {{-- Суто клієнтський стан: сервер не бере участі --}}
    <button x-on:click="open = !open">Деталі</button>

    <div x-show="open" x-transition>
        {{-- Читання властивості компонента --}}
        <p x-text="$wire.title"></p>

        {{-- Запис властивості --}}
        <button x-on:click="$wire.title = 'Новий заголовок'">Перейменувати</button>

        {{-- Виклик методу (повертає Promise) --}}
        <button x-on:click="$wire.save()">Зберегти</button>

        {{-- Метод з очікуванням результату --}}
        <button x-on:click="await $wire.calculate(); open = false">Розрахувати</button>

        {{-- Виклик без перемальовування --}}
        <button x-on:click="$wire.$set('tab', 'stats', false)">Статистика</button>
    </div>
</div>

Двобічний зв'язок: $wire.entangle

Blade
<div x-data="{ query: $wire.entangle('search') }">
    <input x-model="query">
    <p x-show="query.length > 0">Шукаємо: <span x-text="query"></span></p>
</div>

{{-- Відкладена синхронізація: не ганяти запит на кожен символ --}}
<div x-data="{ query: $wire.entangle('search').live }">…</div>

Власні скрипти всередині компонента

Blade
@script
<script>
    // Виконається один раз під час ініціалізації компонента.
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    // $wire доступний і тут.
    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Blade
{{-- Завантажити зовнішню бібліотеку один раз на всю сторінку --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets

Ігнорування піддерева

Якщо сторонній віджет сам керує DOM, morph-алгоритм йому лише заважає.

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

{{-- Ігнорувати лише дітей, але оновлювати атрибути самого елемента --}}
<div wire:ignore.self>…</div>

Глобальні JS-хуки

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

    // Перед надсиланням кожного запиту
    Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        succeed(({ status, json }) => {
            console.debug('Livewire відповів', status);
        });

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();   // сплив CSRF-токен
            }
        });
    });

    // Перед і після morph окремого елемента
    Livewire.hook('morph.updated', ({ el, component }) => {});

    // Компонент ініціалізовано
    Livewire.hook('component.init', ({ component }) => {});
});

// Програмний доступ до компонентів
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());

20. Тестування

Livewire дає власний тестовий API поверх Laravel. Тести швидкі — браузер не потрібен, працює звичайний PHPUnit або Pest.

tests/Feature/CounterTest.php
<?php

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

it('збільшує лічильник', function () {
    Livewire::test(Counter::class)
        ->assertSet('count', 0)
        ->call('increment')
        ->assertSet('count', 1)
        ->call('increment')
        ->assertSet('count', 2)
        ->call('decrement')
        ->assertSet('count', 1);
});
tests/Feature/ContactFormTest.php
use App\Livewire\ContactForm;
use App\Models\Contact;
use Livewire\Livewire;

it('валідує обов\'язкові поля', function () {
    Livewire::test(ContactForm::class)
        ->set('name', '')
        ->set('email', 'не-email')
        ->call('submit')
        ->assertHasErrors([
            'name'  => 'required',
            'email' => 'email',
        ])
        ->assertNoRedirect();
});

it('зберігає коректну заявку', function () {
    Livewire::test(ContactForm::class)
        ->set('name', 'Ірина Ковач')
        ->set('email', 'irina@example.com')
        ->set('message', str_repeat('Потрібен сайт для клініки. ', 3))
        ->set('consent', true)
        ->call('submit')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

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

Основні твердження

МетодПеревіряє
assertSet('prop', $value)Значення властивості
assertNotSet('prop', $value)Значення відрізняється
assertSee('текст')Текст присутній у HTML
assertDontSee('текст')Тексту немає
assertSeeHtml('<b>')Розмітка присутня
assertHasErrors(['email'])Помилки валідації
assertHasNoErrors()Помилок немає
assertDispatched('event')Подію надіслано
assertRedirect(route(…))Виконано редирект
assertStatus(403)HTTP-статус
assertForbidden()Доступ заборонено
assertCount('items', 3)Розмір масиву/колекції

Автентифікація, параметри, події

PHP
// Від імені користувача
Livewire::actingAs($admin)
    ->test(OrderTable::class)
    ->assertSee('Усі замовлення');

// З параметрами mount()
Livewire::test(OrderCard::class, ['order' => $order, 'mode' => 'full'])
    ->assertSee($order->number);

// Приймання події
Livewire::test(OrderList::class)
    ->dispatch('order-created', orderId: 42)
    ->assertSee('Замовлення №42');

// Завантаження файлу
use Illuminate\Http\UploadedFile;

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

// Компонент усередині сторінки
$this->get('/dashboard')
    ->assertSeeLivewire(Dashboard::class)
    ->assertDontSeeLivewire(AdminPanel::class);
Що тестувати насамперед. Авторизацію в діях, правила валідації та переходи станів. Саме там ламається функціональність і саме там ховаються діри в безпеці.

21. Безпека

Головний принцип. Кожен публічний метод компонента — це відкритий HTTP-ендпоінт. Кожну публічну властивість клієнт може змінити. Ставтеся до компонента як до контролера, а не як до внутрішнього класу.

1. Авторизація всередині кожної дії

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

    $this->authorize('delete', $post);   // Policy — обов'язково

    $post->delete();
}
PHP
// Авторизація всього компонента
public function mount(Project $project): void
{
    $this->authorize('view', $project);

    $this->project = $project;
}

2. #[Locked] для ідентифікаторів

PHP
use Livewire\Attributes\Locked;

class InvoiceEditor extends Component
{
    #[Locked]
    public int $invoiceId;      // підмінити з фронтенду не можна

    public string $note = '';
}

Без #[Locked] достатньо одного рядка в консолі браузера:

JavaScript (атака)
Livewire.find('...').set('invoiceId', 999)   // чужий рахунок

3. Правила для прив'язки до моделей

wire:model="post.title" працює, лише якщо поле дозволене правилом валідації — так Livewire захищає від масового присвоєння.

PHP
protected function rules(): array
{
    return [
        'post.title' => 'required|string|max:180',
        'post.body'  => 'required|string',
        // 'post.user_id' свідомо НЕ включаємо — інакше допис можна переписати на іншого автора
    ];
}

4. Екранування виводу

Blade
{{-- Безпечно: екранується --}}
{{ $comment->body }}

{{-- Небезпечно: сирий HTML від користувача --}}
{!! $comment->body !!}

{{-- Якщо HTML потрібен — санітизуйте на сервері --}}
{!! clean($comment->body) !!}

5. Обмеження частоти

PHP
use Illuminate\Support\Facades\RateLimiter;

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

    if (RateLimiter::tooManyAttempts($key, 5)) {
        throw ValidationException::withMessages([
            'email' => 'Забагато спроб. Повторіть за хвилину.',
        ]);
    }

    RateLimiter::hit($key, 60);

    // …
}

6. Не зберігайте секрети у властивостях

PHP
class PaymentForm extends Component
{
    public string $cardLast4 = '';        // ок

    // НЕ МОЖНА: поїде в браузер у відкритому вигляді всередині wire:snapshot
    // public string $apiSecret = '';
    // public string $fullCardNumber = '';

    protected function gateway(): PaymentGateway
    {
        return app(PaymentGateway::class);   // секрети лишаються на сервері
    }
}

Чек-лист перед релізом

  • $this->authorize() є в кожній дії, що змінює дані.
  • Усі ідентифікатори позначені #[Locked].
  • Правила валідації не містять службових полів (user_id, role, price).
  • Публічні властивості не містять токенів, ключів і персональних даних понад потрібне.
  • Завантаження файлів обмежене за MIME-типами й розміром.
  • Форми входу та надсилання повідомлень захищені rate limiting.
  • {!! !!} використовується лише для довіреного або санітизованого HTML.

22. Продуктивність

1. Мінімум стану в публічних властивостях

Погано
public Collection $products;   // 500 моделей їздять на клієнт і назад

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. Не ганяйте запити на кожне натискання клавіші

Blade
{{-- Погано: запит на кожен символ --}}
<input wire:model.live="search">

{{-- Добре --}}
<input wire:model.live.debounce.400ms="search">

3. Дробіть важкі сторінки на компоненти

Оновлення вкладеного компонента не перерендерює батьківський. Дашборд із шести незалежних віджетів працює помітно жвавіше, ніж один моноліт.

4. Кешуйте дорогі обчислення

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

5. Стежте за N+1

PHP
// Погано — запит на кожен рядок у шаблоні
$orders = Order::paginate(50);

// Добре
$orders = Order::with(['customer', 'items.product'])->paginate(50);

Увімкніть суворий режим у розробці, щоб N+1 падав з помилкою:

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

6. wire:key у списках

Без ключів morph-алгоритм перебудовує більше вузлів, ніж потрібно, — і робить це неправильно.

7. Ліниве завантаження важких блоків

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

8. Обережно з polling

wire:poll.visible.10s замість wire:poll заощаджує серверу порядок величини запитів.

СимптомЙмовірна причина
Затримка 300–800 мс на будь-яку діюВажкий render() або N+1
Величезний HTML сторінкиКолекції моделей у публічних властивостях
Зростає навантаження на PHP-FPMАгресивний wire:poll або .live без debounce
Поля «стрибають» під час оновлення спискуБракує wire:key
Перерендерюється вся сторінкаУсе в одному компоненті, немає розбиття

23. Часті помилки та рішення

«Component must have a single root element»

У шаблоні кілька кореневих вузлів або текст/коментар на верхньому рівні. Загорніть усе в один <div>.

Погано
<h1>Заголовок</h1>
<p>Текст</p>
Добре
<div>
    <h1>Заголовок</h1>
    <p>Текст</p>
</div>

«Livewire encountered corrupt data»

Не збігся підпис snapshot. Причини: змінився APP_KEY, сторінка відкрита з кешу після деплою, дві вкладки з різними сесіями. Зазвичай лікується перезавантаженням сторінки; після деплою — php artisan optimize:clear.

Кліки не працюють, консоль порожня

  • Немає @livewireScripts у layout.
  • Підключено другий екземпляр Alpine.js (у Livewire 3 він уже всередині).
  • Помилка JS вище на сторінці обірвала виконання.
  • Елемент усередині wire:ignore.

Значення полів «переїжджають» між рядками

Бракує wire:key/:key у циклі. Ключ має бути стабільним і унікальним.

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

«Unable to set component data. Public property not found»

Ви прив'язали wire:model до неіснуючої або не-публічної властивості. Перевірте написання й модифікатор доступу.

«Cannot bind to model data without validation rules»

Прив'язка виду wire:model="post.title" потребує правила для post.title в rules() або #[Validate].

Сторонній віджет ламається після оновлення

Select2, Flatpickr, TinyMCE і подібні самі змінюють DOM. Загорніть у wire:ignore і синхронізуйте вручну.

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

Завантаження файлу мовчки падає

Перевірте upload_max_filesize і post_max_size у PHP, а також client_max_body_size у nginx. Значення мають перевищувати ліміт у правилі max:.

Модальне вікно не закривається після збереження

Надішліть браузерну подію й обробіть її в Alpine, а не покладайтеся на перемальовування.

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

Скрипти не працюють після wire:navigate

Перенесіть ініціалізацію з DOMContentLoaded у livewire:navigated.

Помилка 419 (Page Expired)

Сплила сесія. Збільште SESSION_LIFETIME або перехопіть статус у хуку request і перезавантажте сторінку — приклад є в розділі 19.

24. Шпаргалка

Директиви Blade

ДирективаПризначення
wire:modelПрив'язка поля до властивості (відкладена)
wire:model.liveПрив'язка із запитом на кожну зміну
wire:model.blurНадсилання при втраті фокусу
wire:clickВиклик методу по кліку
wire:submitОбробка надсилання форми
wire:keydown.enterРеакція на клавішу
wire:changeРеакція на change
wire:confirmПідтвердження перед дією
wire:loadingІндикатор завантаження
wire:targetЗвузити індикатор до дії
wire:dirtyЄ незбережені зміни
wire:offlineНемає з'єднання
wire:pollПеріодичне оновлення
wire:initВикликати метод одразу після рендеру
wire:navigateSPA-перехід за посиланням
wire:keyІдентифікатор елемента в циклі
wire:ignoreВиключити піддерево з morph
wire:transitionАнімація появи/зникнення
wire:streamПотокова віддача вмісту

PHP-атрибути

АтрибутПризначення
#[Validate]Правило валідації властивості
#[Locked]Заборона зміни з фронтенду
#[Computed]Обчислювана властивість із кешем
#[Url]Синхронізація з рядком запиту
#[Session]Зберігання значення в сесії
#[On]Слухач події
#[Reactive]Оновлення параметра від батька
#[Modelable]Підтримка wire:model на компоненті
#[Lazy]Ліниве завантаження компонента
#[Layout]Layout для full-page компонента
#[Title]Заголовок сторінки
#[Renderless]Метод без перемальовування

Корисні методи компонента

PHP
$this->reset();                       // скинути всі властивості до початкових
$this->reset('search', 'page');       // скинути вибрані
$this->only('title', 'body');         // масив із частини властивостей
$this->except('password');            // усе, крім зазначеного
$this->fill(['title' => 'Новий']);    // масове присвоєння
$this->pull('draft');                 // отримати і скинути

$this->validate();
$this->validateOnly('email');
$this->resetValidation();
$this->addError('email', 'Повідомлення');

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

$this->skipRender();                  // не перемальовувати в цьому циклі
$this->js('alert("Готово")');         // виконати JS на клієнті
$this->stream(to: 'answer', content: $chunk);

Artisan-команди

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

Офіційні ресурси