Livewire 3 и 4 — что изменилось
Всё, что изменилось между Livewire 3 и Livewire 4: новые возможности, которые стоит взять, каждое ломающее изменение с кодом «было / стало» и чек-лист самого апгрейда. Включая три изменения, которые проходят ваши тесты и ломают продакшен.
1. Коротко
Livewire 4 — большой релиз с небольшим путём миграции. Модель компонента не изменилась: PHP-класс, Blade-шаблон, состояние на сервере, HTML по проводу. Изменилось то, где лежат компоненты, как они пишутся и какую часть страницы задевает одно обновление.
Что вы получаете
- Однофайловые компоненты — класс и разметка в одном файле.
- Острова — области с независимым рендерингом, без разбиения на дочерние компоненты.
- Слоты — родитель передаёт разметку в потомка, вычисляется в контексте родителя.
- Оптимистичный UI —
wire:show,wire:text,wire:bindменяют DOM без round trip. - Drag and drop —
wire:sort, без внешней библиотеки. - Скоупленный CSS — стили компонента, которые не протекают.
Что подставит
- Незакрытые теги
<livewire:…>молча не рендерятся. - Модификаторы
wire:modelизменили смысл — поля перестают синхронизироваться. - Эндпоинт обновлений переехал на
/livewire-{hash}/— ломаются инфраструктурные правила. - Модификаторы
wire:transitionубраны. - Volt вошёл в ядро.
Каждый пункт разобран ниже с кодом «было / стало».
2. Сравнение бок о бок
| Область | Livewire 3 | Livewire 4 |
|---|---|---|
| Файлы компонента | app/Livewire/Counter.php + resources/views/livewire/counter.blade.php |
resources/views/components/⚡counter.blade.php (один файл) |
| Команда создания | php artisan livewire:make Counter |
php artisan make:livewire counter |
| Маршрутизация | Route::get('/x', Dashboard::class) |
Route::livewire('/x', 'pages::dashboard') |
| Частичный рендеринг | Разбиение на дочерние компоненты | Области @island |
| Слоты | Не поддерживаются | {{ $slot }}, <wire:slot name="…"> |
| Drag and drop | SortableJS плюс обвязка | wire:sort |
| Мгновенный UI | Alpine.js руками | wire:show, wire:text, wire:bind |
| CSS компонента | Глобальный стайлшит | Скоупленный <style> в компоненте |
| Параллельные действия | Недоступны | #[Async], wire:click.async |
| Эндпоинт обновлений | /livewire/update |
/livewire-{hash}/update |
| JS-хуки | Livewire.hook('request'|'commit') |
interceptRequest(), interceptMessage() |
| Volt | Отдельный пакет | Вошёл в ядро |
| Тестирование | PHPUnit или Pest, tests/Feature/Livewire |
Рекомендуется Pest, тесты могут лежать рядом с компонентом |
3. Апгрейд по шагам
# 1. Поднять зависимость
composer require livewire/livewire:^4.0
# 2. Очистить все кеши — устаревшие скомпилированные шаблоны это типовая первая поломка
php artisan optimize:clear
# 3. Прогнать тесты, прежде чем трогать что-то ещё
php artisan testLaravel Shift автоматизирует значительную часть механической работы. Независимо от того, пользуетесь вы им или нет, пройдитесь потом по чек-листу ниже — молчаливые поломки инструмент отмечает хуже всего.
Порядок работ, который обычно проходит гладко
- Обновить пакет и очистить кеши.
- Починить переименования в конфиге (глава 4).
- Починить маршруты (глава 5).
- Найти в коде незакрытые теги компонентов (глава 6).
- Проверить каждый
wire:modelс модификатором (глава 7). - Обновить инфраструктурные правила под новый эндпоинт (глава 8).
- Заменить убранные модификаторы
wire:transition(глава 9). - Мигрировать компоненты Volt, если он использовался (глава 12).
- Прогнать тесты снова и пройти приложение руками в браузере.
- И только потом браться за острова, слоты и однофайловые компоненты.
4. Переименования в конфигурации
| Livewire 3 | Livewire 4 | Примечание |
|---|---|---|
layout | component_layout | Использует namespace layouts:: |
lazy_placeholder | component_placeholder | — |
smart_wire_keys | smart_wire_keys | Умолчание сменилось с false на true |
| — | component_locations | Каталоги, где ищутся компоненты |
| — | component_namespaces | Именованные корни, например pages:: |
| — | make_command | Однофайловая или классовая генерация |
| — | csp_safe | Сборка под строгую CSP |
'layout' => 'layouts.app',
'lazy_placeholder' => 'livewire.placeholder','component_layout' => 'layouts::app',
'component_placeholder' => 'livewire.placeholder',
'component_locations' => [
'resources/views/components',
'resources/views/livewire',
],
'component_namespaces' => [
'pages' => 'resources/views/pages',
],
// Продолжать генерировать классовые компоненты
'make_command' => [
'type' => 'class',
],5. Маршрутизация
Для full-page компонентов появился отдельный макрос маршрута.
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class)
->middleware('auth')
->name('dashboard');use App\Livewire\Dashboard;
// По классу
Route::livewire('/dashboard', Dashboard::class)
->middleware('auth')
->name('dashboard');
// Или по имени компонента, для view-based компонентов
Route::livewire('/dashboard', 'pages::dashboard')
->middleware('auth')
->name('dashboard');grep -rn "Route::get(.*::class" routes/, затем проверить, какие
из этих классов наследуют Livewire\Component.
6. Теги компонентов обязаны закрываться
<livewire:user-profile>
<livewire:order-list><livewire:user-profile />
<livewire:order-list />Как найти
# Теги компонентов, которые не самозакрыты
grep -rn "<livewire:[^>]*[^/]>" resources/views/
# Форма через директиву @livewire() не затронута
grep -rn "@livewire(" resources/views/Когда приложение поднимется, стоит потратить время и пройти основные сценарии руками: пропавший блок легко не заметить в диффе и невозможно не заметить на экране.
7. wire:model изменился дважды
Два независимых изменения, и оба дают «поле просто перестало работать» вместо ошибки. Проверьте каждый
wire:model с модификатором.
7.1 Модификаторы управляют синхронизацией на клиенте
В v3 .blur и .change решали, когда уходит сетевой запрос. Само
значение всегда отслеживалось на клиенте. В v4 они решают, когда значение вообще
синхронизируется. Чтобы вернуть прежнее поведение, добавьте .live.
<input wire:model.blur="title">
<select wire:model.change="status"><input wire:model.live.blur="title">
<select wire:model.live.change="status">7.2 Больше нет всплытия от дочерних элементов
wire:model на обёртке больше не перехватывает события, всплывающие от вложенных полей.
<div wire:model="value">
<input type="text">
</div><div wire:model.deep="value">
<input type="text">
</div>Как найти
# Все wire:model с модификатором
grep -rn "wire:model\.[a-z]" resources/views/
# wire:model на не-input элементе — кандидат на проблему со всплытием
grep -rn "<div[^>]*wire:model" resources/views/8. Эндпоинт обновлений переехал
Все URL Livewire теперь содержат хеш: /livewire/ стал
/livewire-{hash}/.
/livewire/.
Где проверить
- Блоки
locationв nginx или Apache - Белые списки WAF и файрвола
- Правила обхода кеша CDN (page rules в Cloudflare и подобное)
- Rate limiter'ы, привязанные к пути
- Правила маршрутизации балансировщика
- Мониторинг и uptime-проверки, нацеленные на эндпоинт
- Записи
connect-srcв CSP, привязанные к пути
location /livewire/ {
# …
}location ~ ^/livewire(-[a-z0-9]+)?/ {
# …
}9. wire:transition переделан
wire:transition больше не принимает модификаторы вроде .opacity,
.scale или .duration.200ms. Он построен на нативном View Transitions API, а
анимация описывается в CSS.
<div wire:transition.opacity.duration.300ms>
…
</div><div wire:transition>
…
</div>::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 300ms;
}10. Изменения в PHP API
Стриминг
$this->stream(to: '#container', content: 'Hello', replace: true);$this->stream('Hello', replace: true, el: '#container');Монтирование компонента
mount($name, $params = [], $key = null)mount($name, $params = [], $key = null, $slots = [])Актуально, если вы монтируете компоненты программно или у вас есть свой код-обёртка над монтированием Livewire. Обычный прикладной код это не затрагивает.
Новые атрибуты, о которых стоит знать
| Атрибут | Назначение |
|---|---|
#[Async] | Выполнить действие параллельно, вне очереди запросов |
#[Renderless] | Пропустить перерисовку после действия |
#[Prop] | Объявить свойство как props от родителя |
#[Json] | Вернуть данные прямо в JavaScript |
11. Изменения в JavaScript API
Действия $js
$wire.$js('showToast', { message: 'Сохранено' });$wire.$js.showToast = { message: 'Сохранено' };Хуки стали перехватчиками
Хуки commit и request объявлены устаревшими в пользу
interceptMessage() и interceptRequest().
document.addEventListener('livewire:init', () => {
Livewire.hook('request', ({ options, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload();
}
});
});
});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 x-show="$errors.has('email')" x-text="$errors.first('email')"></div>12. Volt вошёл в ядро
Однофайловый синтаксис Volt стал прототипом того, чем в Livewire 4 стали однофайловые компоненты. Отдельный пакет больше не нужен.
| Volt | Livewire 4 |
|---|---|
Livewire\Volt\Component | Livewire\Component |
Volt::route() | Route::livewire() |
Volt::test() | Livewire::test() |
| Сервис-провайдер Volt | Удалить |
Пакет livewire/volt | Удалить зависимость |
composer remove livewire/volt<?php
use Livewire\Volt\Component;
new class extends Component {
public string $title = '';
};
?>
<div>…</div><?php
use Livewire\Component;
new class extends Component {
public string $title = '';
};
?>
<div>…</div>13. Освоение новых возможностей
Когда приложение зелёное на v4, вот изменения, которые окупаются быстрее всего — примерно в порядке отдачи на усилие.
1. Острова на самой медленной странице
Найдите страницу, где одно маленькое взаимодействие перезапускает кучу запросов — обычно это дашборд
или таблица с фильтрами. Оберните независимые области в @island.
<div>
<div class="card">Выручка: {{ $this->revenue }}</div>
<div class="card">Задач в очереди: {{ $this->queue }}</div>
<div class="card">Последняя активность: …</div>
</div><div>
@island(name: 'revenue')
<div class="card">Выручка: {{ $this->revenue }}</div>
@endisland
@island(name: 'queue', poll: '5s')
<div class="card">Задач в очереди: {{ $this->queue }}</div>
@endisland
@island(name: 'activity', lazy: true)
@placeholder
<div class="card skeleton animate-pulse h-48"></div>
@endplaceholder
<div class="card">Последняя активность: …</div>
@endisland
</div>2. Заменить round trip клиентскими директивами
<button wire:click="$toggle('showFilters')">Фильтры</button>
@if ($showFilters)
<div class="filters">…</div>
@endif<button wire:click="$toggle('showFilters')">Фильтры</button>
<div class="filters" wire:show="showFilters">…</div>3. Выбросить SortableJS
<ul wire:sort="reorder">
@foreach ($tasks as $task)
<li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
<span wire:sort:handle>⠿</span> {{ $task->title }}
</li>
@endforeach
</ul>4. Схлопнуть дочерние компоненты, созданные только ради рендера
В v3 стандартным лекарством от «слишком много перерисовывается» был дочерний компонент. Если у потомка нет собственного состояния и он не переиспользуется, остров в родителе легче — один компонент, один класс, один snapshot.
5. #[Renderless] для невидимой работы
use Livewire\Attributes\Renderless;
#[Renderless]
public function trackView(): void
{
$this->post->increment('views');
}6. Перевод на однофайловый формат — последним и только там, где помогает
Это косметика. Делайте по случаю, когда и так правите файл, а не отдельным спринтом миграции. Мелкие компоненты выигрывают; компоненты с тяжёлой логикой и множеством зависимостей часто понятнее классами.
14. Чек-лист апгрейда
До начала
- Тесты проходят на Livewire 3.
- Отдельная ветка и бэкап базы, если окружение общее.
- Кто-то готов пройти приложение руками после — молчаливые поломки видит только человек.
Механические правки
composer require livewire/livewire:^4.0php artisan optimize:clearlayout→component_layout(и namespacelayouts::)lazy_placeholder→component_placeholderRoute::get(…, Component::class)→Route::livewire(…)- Все теги
<livewire:…>самозакрыты wire:model.blur/.change→ добавить.livewire:modelна обёртке → добавить.deepwire:scroll→wire:navigate:scroll- Модификаторы
wire:transition→ в CSS - Порядок аргументов
$this->stream() $wire.$js('name', …)→$wire.$js.name = …Livewire.hook('request'|'commit')→interceptRequest()/interceptMessage()- Классы, маршруты, тесты и пакет Volt удалены
Инфраструктура
- Location-блоки nginx / Apache расширены под
/livewire-{hash}/ - Правила WAF, CDN и файрвола обновлены
- Rate limiter'ы и мониторинг перенацелены
connect-srcв CSP пересмотрен
Проверка
- Тесты зелёные
- Все формы отправляются и валидируются
- Загрузка файлов работает, включая индикатор прогресса
- Таблицы листаются, сортируются и фильтруются
- Модальные окна открываются и закрываются
- Сторонние виджеты внутри
wire:ignoreинициализируются - Консоль браузера чистая на основных сценариях
- Запросы обновления возвращают 200 на проде, а не 403 от WAF
Официальные ресурсы
- Upgrade guide: livewire.laravel.com/docs/upgrading
- Документация Livewire: livewire.laravel.com/docs