Livewire 4 — повний посібник
Livewire 4 зберігає знайому модель компонента і перебудовує все навколо неї: однофайлові компоненти, острови з незалежним рендерингом, слоти в контексті батька, вбудований drag-and-drop і клієнтські директиви, які взагалі не ходять на сервер. Посібник покриває фреймворк цілком, з явним зазначенням відмінностей четвертої версії.
1. Що змінює Livewire 4
Livewire 4 — найбільший реліз в історії фреймворку. Сама модель компонента не змінилася: PHP-клас, Blade-шаблон, стан на сервері, HTML по дроту. Змінилося все навколо: де лежать компоненти, як вони пишуться і яку частину сторінки зачіпає одне оновлення.
П’ять головних речей
| Можливість | Що вона дає |
|---|---|
| Однофайлові компоненти | Клас і розмітка в одному .blade.php. Більше не треба стрибати між двома каталогами заради компонента на двадцять рядків. |
| Острови (islands) | Ізольовані ділянки всередині компонента, які перемальовуються самі по собі. Лічильник виручки більше не перезапускає запити всього дашборда. |
| Слоти | Батько передає розмітку в дочірній компонент, і вона обчислюється в контексті батька: wire:click усередині слота викликає метод батька. |
| Оптимістичний UI | wire:show, wire:text, wire:bind змінюють DOM миттєво, без звернення до сервера. |
| Drag and drop | wire:sort вбудований. Ні SortableJS, ні склеювального коду. |
Це переписування застосунку?
Ні. Livewire 4 зберігає високу зворотну сумісність: класові компоненти продовжують працювати. Однофайловий формат — умовчання для нових компонентів, а не примусова міграція. Є набір ламких змін, які варто прочитати до апгрейду — вони зібрані в порівнянні Livewire 3 і 4, а офіційний upgrade guide залишається першоджерелом.
wire:model, острови та слоти. Решта моделі компонента
здасться знайомою.
2. Встановлення та вимоги
composer require livewire/livewire:^4.0
php artisan optimize:clear
Як і в третій версії, ассети підключаються автоматично. @livewireStyles і
@livewireScripts потрібно ставити вручну лише за нестандартного layout або суворої
Content Security Policy.
/livewire/ став /livewire-{hash}/. Якщо у вас є правила фаєрвола, винятки
WAF, обходи CDN або location-блоки nginx, прив’язані до буквального шляху /livewire/,
їх треба розширити під новий шаблон — інакше на проді застосунок виглядатиме зламаним, ідеально
працюючи на ноутбуці.
Публікація конфігурації
php artisan livewire:publish --configНові та перейменовані параметри четвертої версії:
| Параметр | Призначення |
|---|---|
component_locations | Каталоги, де шукаються компоненти. За замовчуванням resources/views/components і resources/views/livewire. |
component_namespaces | Іменовані корені, наприклад pages::. |
component_layout | Був layout у v3. Використовує namespace layouts::. |
component_placeholder | Був lazy_placeholder у v3. |
make_command | Що генерує make:livewire: однофайловий чи класовий компонент. |
smart_wire_keys | Тепер за замовчуванням true. |
csp_safe | Збірка, сумісна із суворою CSP. |
Вибір стилю компонентів за замовчуванням
// Продовжувати генерувати класові компоненти замість однофайлових
'make_command' => [
'type' => 'class',
],3. Однофайлові компоненти
Головна зміна в повсякденній роботі. Компонент — це один Blade-файл, що починається з PHP-блоку з анонімним класом, а далі йде розмітка.
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: 'Допис створено');
}
};
?>
<div>
<form wire:submit="save">
<input type="text" wire:model="title" placeholder="Заголовок">
@error('title') <span class="error">{{ $message }}</span> @enderror
<textarea wire:model="body" placeholder="Текст"></textarea>
@error('body') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Зберегти</button>
</form>
</div>
Публічні властивості доступні в розмітці як звичайні змінні — {{ $title }} працює так
само, як працювало в окремому файлі шаблону.
Про емодзі в імені
За замовчуванням ім’я файлу починається з ⚡. Це візуальний маркер, який відрізняє
Livewire-компоненти від звичайних Blade-компонентів у тому ж каталозі, і він
необов’язковий — у config/livewire.php його можна вимкнути, якщо так
зручніше інструментам, терміналу чи команді.
Багатофайлові компоненти
Коли компонент переростає комфортний розмір одного файлу, згенеруйте його багатофайловим — клас, шаблон і тест знову опиняться окремо.
php artisan make:livewire post.create --mfcКласові компоненти працюють далі
<?php
namespace App\Livewire;
use Livewire\Component;
class CreatePost extends Component
{
public string $title = '';
public function render()
{
return view('livewire.create-post');
}
}app/Livewire.
4. Розташування, іменування та namespace
Компоненти Livewire більше не живуть за замовчуванням в окремому «загоні»
resources/views/livewire; вони лежать поруч з іншими Blade-компонентами.
resources/views/
├── components/
│ ├── ⚡counter.blade.php → <livewire:counter />
│ ├── post/
│ │ ├── ⚡create.blade.php → <livewire:post.create />
│ │ └── ⚡index.blade.php → <livewire:post.index />
│ └── button.blade.php (звичайний Blade-компонент)
└── pages/
└── ⚡dashboard.blade.php → <livewire:pages::dashboard />Виведення
{{-- За іменем --}}
<livewire:counter />
{{-- Вкладені каталоги через крапку --}}
<livewire:post.create />
{{-- З namespace --}}
<livewire:pages::post.create />
{{-- З параметрами --}}
<livewire:post.create :title="$initialTitle" :author="$user" /><livewire:some-component> просто не рендериться — без помилки, без виводу. Завжди
самозакривайте: <livewire:some-component />. Це найчастіша несподіванка під час
перенесення Blade-файлів з v3.
Маршрут на компонент
use App\Livewire\Dashboard;
// За класом
Route::livewire('/dashboard', Dashboard::class);
// За іменем компонента — звичний вибір для view-based компонентів
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('Панель керування')]
public function render() { /* … */ }
};5. Властивості та props
Публічні властивості так само утворюють стан компонента, так само серіалізуються у snapshot між
запитами і підпорядковані тим самим обмеженням за типами: скаляри, масиви скалярів, Eloquent-моделі
та колекції, Carbon, enum.
new class extends Component {
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
};Props від батька: #[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">
Рахунок прострочено.
</livewire:alert>mount() так само перший
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] важливий так само
use Livewire\Attributes\Locked;
new class extends Component {
#[Locked]
public int $invoiceId; // клієнт не підставить чужий id
public string $note = '';
};6. wire:model у четвертій версії
wire:model поводяться інакше, і обидва ламаються мовчки, без помилки.
Зміна 1: модифікатори керують синхронізацією на клієнті
У v3 .blur і .change вирішували лише, коли йде мережевий запит.
У v4 вони вирішують, коли значення взагалі синхронізується — зокрема на клієнті. Щоб
повернути попередню поведінку, додайте .live.
{{-- Livewire 3 --}}
<input wire:model.blur="title">
{{-- Еквівалент тієї ж поведінки у v4 --}}
<input wire:model.live.blur="title">Зміна 2: події більше не спливають від дітей
{{-- Livewire 3: обгортка ловила подію поля --}}
<div wire:model="value">
<input type="text">
</div>
{{-- Livewire 4: вмикаємо явно --}}
<div wire:model.deep="value">
<input type="text">
</div>Решта без змін
{{-- За замовчуванням відкладено --}}
<input type="text" wire:model="name">
{{-- Запит на кожну зміну --}}
<input type="text" wire:model.live="search">
{{-- Живий пошук із затримкою --}}
<input type="text" wire:model.live.debounce.400ms="search">
{{-- Вкладені дані --}}
<input wire:model="form.address.city">
<input wire:model="post.title">post.title) так само потребує відповідного правила
валідації. Це захист від масового присвоєння, і він перейшов у v4 без змін.
7. Дії, async і renderless
Дії оголошуються як і раніше — публічні методи, які викликаються з розмітки.
<button wire:click="save">Зберегти</button>
<form wire:submit="save">…</form>
<button wire:click="delete({{ $post->id }})"
wire:confirm="Видалити допис? Скасувати буде неможливо.">Видалити</button>Дії без перемальовування
use Livewire\Attributes\Renderless;
new class extends Component {
#[Renderless]
public function incrementViewCount(): void
{
$this->post->increment('views');
}
};<button type="button" wire:click.renderless="incrementViewCount">Врахувати</button>Асинхронні дії
#[Async] (або модифікатор .async) виконує дію паралельно,
в обхід звичайної черги запитів. Ідеально для «вистрілив і забув»: аналітика, логування, прогрів кешу.
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')">Почати</button>Магічні дії
<button wire:click="$refresh">Оновити</button>
<button wire:click="$set('tab', 'settings')">Налаштування</button>
<button wire:click="$toggle('showFilters')">Фільтри</button>
<button wire:click="$dispatch('open-modal', { name: 'create' })">Створити</button>
<button wire:click="$parent.closeModal()">Закрити</button>protected або private, щоб їх не можна було викликати з клієнта.
8. Життєвий цикл і хуки
Життєвий цикл не змінився порівняно з третьою версією.
1. Створюється екземпляр компонента
2. boot()
3. mount($params)
4. booted()
5. render() 1. Надходить POST /livewire-{hash}/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. Виконуються дії з черги
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — стан пакується назад у snapshot
14. Відповідь: розмітка + 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 updating(string $property, mixed $value): void {}
public function updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
public function updatedFormEmail(mixed $value): void
{
$this->form['email'] = strtolower(trim($value));
}
};9. Валідація та Form-об’єкти
Валідація перейшла з v3 без змін, включно з атрибутами і Form-об’єктами.
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: 'Потрібна згода на обробку даних.')]
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-об’єкти
<?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'));
}
}Помилки в JavaScript
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>10. Острови (islands)
Острови — візитівка Livewire 4 і та можливість, що змінює уявлення про розмір компонента. Острів — це ділянка всередині компонента, яка перемальовується незалежно: при оновленні перераховується і патчиться лише цей фрагмент, а не весь компонент.
@island
<div>Виручка: {{ $this->revenue }}</div>
@endislandНавіщо це
У v3 стандартними ліками від «дашборд гальмує» було розбиття на шість дочірніх компонентів, щоб оновлення одного віджета не перезапускало запити решти п’яти. Острови дають ту саму ізоляцію без межі компонента: один компонент, один клас, кілька незалежно оновлюваних ділянок.
<?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>Виручка за місяць</h3>
<p class="stat">{{ number_format($this->revenue, 2, ',', ' ') }} €</p>
</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">
<h3>Завдань у черзі</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>Опції
| Опція | Дія |
|---|---|
name | Ім’я острова, щоб на нього могли націлитися дії та JavaScript. Кілька островів з одним іменем завжди рендеряться групою. |
lazy | Рендериться, коли потрапляє у в’юпорт. |
defer | Рендериться одразу після завантаження сторінки, незалежно від видимості. |
always | Примусово оновлюється при кожному рендері батька. |
skip | Пропускає первинний рендер. |
Націлювання на острів із дії
@island(name: 'revenue')
Виручка: {{ $this->revenue }}
@endisland
<button wire:click="$refresh" wire:island="revenue">Оновити виручку</button>Append і prepend — нескінченна стрічка без обв’язки
@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">Показати ще</button><button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
Показати ще
</button>Опитування в межах острова
@island(name: 'queue')
<div wire:poll.3s>
Завдань у черзі: {{ $this->queue }}
</div>
@endisland11. Слоти та передача атрибутів
Livewire-компоненти тепер приймають вміст слота так само, як це завжди вміли Blade-компоненти — з
однією важливою особливістю: вміст слота обчислюється в контексті батька.
wire:click, написаний усередині слота, викликає метод батька, а не нащадка.
Слот за замовчуванням
<livewire:modal>
<h2>Створити допис</h2>
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Зберегти</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>Іменовані слоти
<livewire:modal>
<wire:slot name="header">
<h2>Створити допис</h2>
</wire:slot>
<form wire:submit="save">
<input wire:model="title">
</form>
<wire:slot name="footer">
<button wire:click="save">Зберегти</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>Передача атрибутів
<?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">
Рахунок прострочено на 14 днів.
</livewire:alert>12. Вкладені компоненти та wire:key
<div>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>Розумні ключі
У четвертій версії smart_wire_keys увімкнено за замовчуванням, тож Livewire виводить
ключі сам у багатьох ситуаціях із циклами, де раніше потрібен був явний.
wire:key,
прив’язаний до id запису.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeachРеактивні props
use Livewire\Attributes\Reactive;
new class extends Component {
#[Reactive]
public int $quantity;
};Звернення до батька
<button wire:click="$parent.refreshList()">Оновити список</button>Двобічна прив’язка на компоненті
use Livewire\Attributes\Modelable;
new class extends Component {
#[Modelable]
public int $value = 0;
};13. Події
API подій не змінився порівняно з третьою версією.
$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: 'Замовлення збережено');<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. Обчислювані властивості
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>Сума: {{ 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, ніколи не $products.
15. Стан в URL і сесії
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', 'Налаштування збережено');
}#[Url]-властивість при зміні смикає History API. Для полів, що змінюються на кожне
натискання клавіші, комбінуйте з wire:model.live.debounce.
16. Завантаження файлів
<?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">Завантажуємо…</div>
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Прев’ю" class="preview">
@endif
@error('avatar') <p class="error">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Зберегти</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, post_max_size у PHP і
client_max_body_size у nginx мають перевищувати ваше правило max:.
17. Пагінація
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),
];
}
};Острови перетворюють пагінацію на нескінченну стрічку
<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">Показати ще</button>
</div>paginate() коштує додаткового COUNT(*).
simplePaginate() помітно дешевший.
18. Стани завантаження
<div wire:loading>Завантаження…</div>
<div wire:loading.remove>Вміст</div>
<button wire:click="save">Зберегти</button>
<span wire:loading wire:target="save">Зберігаємо…</span>
<button wire:click="save" wire:loading.attr="disabled">Зберегти</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Зберегти</button>
<div wire:loading.delay>Завантаження…</div>
<div wire:loading.delay.long>Завантаження…</div>Стилізація з CSS
[data-loading] .btn {
opacity: 0.5;
pointer-events: none;
}<input wire:model="title">
<span wire:dirty wire:target="title">Є незбережені зміни</span>
<div wire:offline class="banner banner--warn">
Немає з’єднання із сервером.
</div>19. Оптимістичний UI на клієнті
Цілому класу взаємодій сервер ніколи не був потрібен: відкрити панель, показати лічильник символів, підсвітити поле червоним. У четвертій версії для них є повноцінні директиви, які змінюють DOM миттєво, взагалі без запиту.
{{-- Перемикає видимість через CSS, миттєво --}}
<div wire:show="showModal" class="modal">…</div>
<button wire:click="$toggle('showModal')">Відкрити</button>{{-- Текст слідує за властивістю на клієнті --}}
<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'">
Стає задовго
</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]);
}
}
};Ручки перетягування
<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 }})">Видалити</button>
</li>
@endforeach
</ul>Перетягування між списками
<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. Скоуплений CSS і скрипти
<?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 }} € / місяць</p>
</div>
<style>
/* Скоупиться цим компонентом — жодних глобальних колізій по .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
<div x-data="{ open: false }">
<button x-on:click="open = !open">Деталі</button>
<div x-show="open">
<p x-text="$wire.title"></p>
<button x-on:click="$wire.save()">Зберегти</button>
<button x-on:click="await $wire.calculate(); open = false">Розрахувати</button>
</div>
</div>wire:ref
<livewire:modal wire:ref="modal" />
<button x-on:click="$refs.modal.open()">Відкрити модалку</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();
}
};Перехоплювачі
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('компонент оновлено', component.name);
});
});
});<div wire:ignore>
<select id="select2-field">…</select>
</div>24. Тестування
Pest — рекомендований спосіб тестувати компоненти Livewire 4.
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.phpuse Livewire\Livewire;
it('рендериться', function () {
Livewire::test('post.create')
->assertStatus(200);
});
it('валідує обов’язкові поля', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors(['title' => 'required']);
});
it('зберігає коректний допис', function () {
Livewire::actingAs($user)
->test('post.create')
->set('title', 'Цілком розумний заголовок')
->set('body', str_repeat('Достатньо довгий текст допису. ', 3))
->call('save')
->assertHasNoErrors()
->assertDispatched('notify');
});| Метод | Перевіряє |
|---|---|
assertSet() / assertNotSet() | Значення властивостей |
assertSee() / assertDontSee() | Вивід |
assertHasErrors() / assertHasNoErrors() | Валідацію |
assertDispatched() | Події |
assertRedirect() | Редиректи |
assertForbidden() | Авторизацію |
25. Безпека та продуктивність
Чек-лист безпеки
$this->authorize()у кожній дії, що змінює дані.- Усі ідентифікатори позначені
#[Locked]. - Допоміжні методи оголошені
protectedабоprivate. - Правила валідації не містять службових полів (
user_id,role,price). - Ні токенів, ні ключів у публічних властивостях — snapshot їде у браузер відкритим текстом.
- Завантаження файлів обмежене за MIME-типом і розміром.
- Форми входу за rate limiter.
- Оптимістичний UI ніколи не вважається достовірним.
Продуктивність
| Симптом | Рішення у v4 |
|---|---|
| Оновлення віджета перезапускає всі запити сторінки | Обгорнути ділянки в @island |
| Величезний HTML сторінки | Прибрати колекції моделей з публічних властивостей |
| Запит на кожне натискання клавіші | wire:model.live.debounce.400ms |
| Навантаження від опитування | Опитування всередині острова плюс .visible |
| Round trip заради перемикання панелі | wire:show замість дії |
| Інкремент лічильника перемальовує сторінку | #[Renderless] |
| Запити N+1 | Жадібне завантаження через with() |
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}26. Часті помилки та шпаргалка
Компонент взагалі не рендериться
Тег не самозакритий. Пишіть <livewire:my-component />.
На проді все 404, локально працює
Ендпоінт Livewire переїхав з /livewire/ на /livewire-{hash}/. Правило
фаєрвола, виняток WAF або location у nginx блокують запити оновлення.
Поле перестало синхронізуватися після апгрейду
Додайте .live: wire:model.live.blur="title".
Обгортка перестала підхоплювати своє поле
Використовуйте wire:model.deep.
Модифікатори wire:transition нічого не роблять
Їх прибрано — v4 використовує нативний View Transitions API. Опишіть анімацію в CSS.
Острів не оновлюється
- В острова немає
name, а дія цілиться в ім’я. wire:island="…"вказує на ім’я, якого немає у виводі.- Ділянка залежить від стану батька, який фактично не змінювався — додайте
always: true.
Компоненти Volt перестали працювати
Volt увійшов у ядро. Замініть Livewire\Volt\Component на Livewire\Component,
Volt::route() на Route::livewire(), видаліть сервіс-провайдер і пакет.
Шпаргалка по директивах
| Директива | Призначення | Нове у v4 |
|---|---|---|
wire:model | Прив’язка поля до властивості | змінилася семантика |
wire:model.deep | Ловити події від дочірніх елементів | так |
wire:click.async | Виконати дію паралельно | так |
wire:click.renderless | Пропустити перемальовування | так |
wire:island | Націлитися на острів | так |
wire:island.append | Дописати в острів | так |
wire:show | Перемкнути видимість на клієнті | так |
wire:text | Прив’язати текст на клієнті | так |
wire:bind | Реактивно прив’язати атрибут | так |
wire:sort | Перетягування і сортування | так |
wire:intersect | Дія при потраплянні у в’юпорт | так |
wire:ref | Ім’я елемента для JS | так |
wire:navigate:scroll | Зберегти скрол контейнера | перейменовано |
wire:poll | Періодичне оновлення | у межах острова |
wire:key | Ідентичність елемента в циклі | розумні ключі за замовчуванням |
Директиви Blade
| Директива | Призначення |
|---|---|
@island … @endisland | Незалежно рендерована ділянка |
@placeholder … @endplaceholder | Заглушка для лінивого острова |
<wire:slot name="…"> | Вміст іменованого слота |
@script … @endscript | JavaScript у межах компонента |
@assets … @endassets | Ассети сторінки, що завантажуються один раз |
<style> / <style global> | Скоуплений / глобальний CSS |
PHP-атрибути
| Атрибут | Призначення |
|---|---|
#[Validate] / #[Locked] | Валідація і захист від запису |
#[Computed] | Кешоване похідне значення |
#[Url] / #[Session] | Збереження стану |
#[On] / #[Prop] | Події та props |
#[Async] / #[Renderless] | Паралельність і пропуск рендеру |
#[Json] | Дані прямо в JavaScript |
php artisan make:livewire post.create # однофайловий
php artisan make:livewire post.create --mfc # багатофайловий
php artisan make:livewire post.create --test # з тестом
php artisan livewire:publish --config
php artisan optimize:clearОфіційні ресурси
- Документація Livewire: livewire.laravel.com/docs
- Upgrade guide: livewire.laravel.com/docs/upgrading
- Alpine.js: alpinejs.dev