Livewire 3 — полное руководство
Livewire позволяет строить динамические интерфейсы в Laravel на PHP и Blade — без отдельного фронтенд-приложения и без API-слоя. Это руководство проводит от установки до продакшена: компоненты, состояние, формы, события, загрузка файлов, тестирование, безопасность и производительность.
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 3.x. Требования: PHP 8.1+, Laravel 10 и выше.
В третьей версии Alpine.js входит в комплект, изменились имена событий (dispatch вместо
emit), появились PHP-атрибуты (#[Computed], #[Validate],
#[Url]), а wire:model по умолчанию стал отложенным.
2. Установка и настройка
Установка в существующий Laravel-проект занимает одну команду.
composer require livewire/livewire
В Livewire 3 отдельно подключать ассеты не обязательно: пакет сам вставляет
@livewireStyles и @livewireScripts в layout. Но если вы используете
нестандартный layout или CSP, директивы можно проставить вручную.
<!DOCTYPE html>
<html lang="ru">
<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>@livewireScripts либо он стоит до подключения вашего собственного
Alpine.js. Livewire 3 уже содержит Alpine — второй экземпляр Alpine ломает реактивность.
Публикация конфигурации
php artisan livewire:publish --configОсновные параметры config/livewire.php:
| Параметр | Назначение |
|---|---|
class_namespace | Namespace классов компонентов. По умолчанию App\Livewire. |
view_path | Каталог Blade-шаблонов компонентов. |
layout | Layout для full-page компонентов. |
temporary_file_upload | Диск, срок жизни и правила для временных загрузок. |
inject_assets | Автоматическая вставка CSS/JS. Отключите, если ставите директивы вручную. |
navigate.show_progress_bar | Полоса прогресса для wire:navigate. |
Проверка установки
php artisan livewire:make Counter
# CLASS: app/Livewire/Counter.php
# VIEW: resources/views/livewire/counter.blade.php3. Первый компонент
Компонент состоит из двух файлов: класса и шаблона.
<?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');
}
}<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><div>.
Три способа вывести компонент
{{-- 1. Тег-синтаксис (рекомендуется) --}}
<livewire:counter />
{{-- 2. Директива --}}
@livewire('counter')
{{-- 3. С параметрами --}}
<livewire:counter :start="10" title="Счётчик заказов" />Full-page компоненты
Компонент можно повесить прямо на маршрут — тогда он рендерится как самостоятельная страница.
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class)->middleware('auth')->name('dashboard');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-файл не нужен.
public function render()
{
return <<<'BLADE'
<div>
<button wire:click="$refresh">Обновить</button>
</div>
BLADE;
}4. Свойства компонента
Публичные свойства класса автоматически доступны в Blade-шаблоне и сохраняются между запросами. Именно они образуют состояние компонента.
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 и т. п. |
Инициализация: mount()
Метод mount() — конструктор компонента. Он выполняется один раз, при первом рендере, и
получает параметры, переданные из Blade.
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;
}
}<livewire:order-card :order="$order" mode="full" :key="$order->id" />Защита свойств: #[Locked]
По умолчанию клиент может подменить значение любого публичного свойства. Для идентификаторов и всего,
что влияет на права доступа, это дыра. Атрибут #[Locked] запрещает изменение свойства
с фронтенда.
use Livewire\Attributes\Locked;
class InvoiceEditor extends Component
{
#[Locked]
public int $invoiceId; // клиент не сможет подставить чужой id
public string $comment = '';
}Скрытые от JSON свойства
Свойства с модификаторами protected и private не сериализуются и
сбрасываются между запросами. Используйте их только для значений, вычисляемых
заново в каждом цикле.
class Report extends Component
{
public string $period = 'month';
// Сбросится после каждого запроса — не храните тут состояние.
protected array $cache = [];
}5. wire:model и привязка данных
wire:model связывает поле формы со свойством компонента. В Livewire 3 привязка
по умолчанию отложенная: значение уезжает на сервер не на каждое нажатие клавиши,
а вместе со следующим действием (клик, submit).
{{-- Отложенно: значение уйдёт со следующим запросом --}}
<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 | Предзаполненные формы |
Все типы полей
{{-- Текст, 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>Вложенные данные
Точечная нотация работает и для массивов, и для свойств моделей.
public array $form = [
'name' => '',
'address' => ['city' => '', 'street' => ''],
];
public Post $post;<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() + обработчик на сервере.
{{-- Клик --}}
<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>Параметры
<button wire:click="delete({{ $post->id }})">Удалить</button>
<button wire:click="setStatus('published')">Опубликовать</button>
<button wire:click="move({{ $item->id }}, 'up')">Вверх</button>public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post); // авторизацию проверяем всегда
$post->delete();
$this->dispatch('notify', message: 'Пост удалён');
}delete(999) с произвольным id. Проверка прав внутри
метода обязательна.
Model binding в параметрах
Livewire умеет разрешать модели по id, как это делает роутер Laravel.
public function archive(Post $post): void
{
$this->authorize('update', $post);
$post->update(['archived_at' => now()]);
}Модификаторы действий
{{-- Подтверждение в браузере до отправки --}}
<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() | Вызвать метод родительского компонента |
<button wire:click="$refresh">Обновить</button>
<button wire:click="$set('tab', 'settings')">Настройки</button>
<button wire:click="$toggle('showFilters')">Фильтры</button>
<button wire:click="$parent.closeModal()">Закрыть</button>Редиректы
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».
render() или через
#[Computed].
Что происходит с DOM
Livewire не заменяет узел целиком, а выполняет morph: обходит старое и новое дерево и меняет
только различия. Поэтому фокус в поле ввода, позиция скролла и состояние Alpine сохраняются. Если
структура списка меняется, morph-алгоритму нужны подсказки — см. wire:key в главе 10.
8. Хуки жизненного цикла
Хуки позволяют вклиниться в любую фазу цикла.
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');
}
}Правила именования
| Свойство | Хук |
|---|---|
$price | updatedPrice() |
$isActive | updatedIsActive() |
$form['email'] | updatedFormEmail() |
$post->title | updatedPostTitle() |
public function updatedSearch() { $this->resetPage(); }
9. Валидация и Form-объекты
Livewire использует валидатор Laravel — правила, сообщения и локализация те же.
Способ 1: атрибуты (Livewire 3)
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()
Нужен, когда правила зависят от состояния.
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' => 'Такой e-mail уже зарегистрирован.',
];
}
protected function validationAttributes(): array
{
return [
'email' => 'адрес электронной почты',
];
}Живая валидация
public function updated(string $property): void
{
$this->validateOnly($property); // проверяем только изменённое поле
}<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>Ручное управление ошибками
$this->addError('email', 'Домен в чёрном списке.');
$this->resetValidation('email');
$this->resetValidation(); // сбросить все
$this->validateOnly('email');
// Проброс ошибки как в обычном Laravel-контроллере
throw ValidationException::withMessages([
'code' => 'Неверный код подтверждения.',
]);Form-объекты
Когда форма разрастается, выносите её в отдельный класс. Компонент остаётся тонким, а правила и данные формы можно переиспользовать между «создать» и «редактировать».
php artisan livewire:form PostForm<?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'));
}
}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);
}
}<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
Компоненты вкладываются друг в друга. Каждый вложенный компонент — независимая единица со своим состоянием и своим циклом обновления: клик внутри дочернего компонента не перерендеривает родителя.
<div>
<h1>Заказы</h1>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>:key обязателен в циклах. Без уникального ключа morph-алгоритм
перепутает элементы при сортировке, фильтрации или удалении: значения полей «переедут» на соседние
строки. Ключ должен быть стабильным — $loop->index не подходит, используйте id записи.
Передача данных вниз
Параметры передаются один раз, в mount(). При последующих рендерах родителя дочерний
компонент не обновляется автоматически — он живёт своей жизнью.
use Livewire\Attributes\Reactive;
class OrderTotal extends Component
{
// С #[Reactive] значение будет приезжать из родителя при каждом его рендере.
#[Reactive]
public int $quantity;
}Обращение к родителю
<button wire:click="$parent.refreshList()">Обновить список</button>Двусторонняя связь: #[Modelable]
Позволяет использовать wire:model на самом компоненте — удобно для полей-виджетов.
use Livewire\Attributes\Modelable;
class RatingInput extends Component
{
#[Modelable]
public int $value = 0;
public function set(int $value): void
{
$this->value = $value;
}
}{{-- В родительском шаблоне --}}
<livewire:rating-input wire:model.live="review.rating" />Условный рендер вложенных компонентов
@if ($showDetails)
<livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif11. События
События — способ связать компоненты, которые не находятся в отношениях «родитель — потомок».
В Livewire 3 отправка — это dispatch() (в версии 2 было emit()).
Отправка
// Простое событие
$this->dispatch('post-created');
// С именованными параметрами
$this->dispatch('post-created', postId: $post->id, title: $post->title);
// Только конкретному компоненту
$this->dispatch('refresh')->to(OrderList::class);
// Только самому себе
$this->dispatch('recalculate')->self();{{-- Прямо из шаблона --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">Новый заказ</button>Приём
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):
protected $listeners = [
'post-created' => 'onPostCreated',
'refresh' => '$refresh',
];События в браузер
Livewire может отправить обычное DOM-событие — его поймает Alpine.js или ваш собственный JS. Это правильный способ показывать тосты и открывать модальные окна.
$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>document.addEventListener('notify', (event) => {
console.log(event.detail.message);
});12. Вычисляемые свойства
#[Computed] — способ получать производные данные без хранения их в состоянии. Результат
кэшируется на время одного запроса, поэтому обращение к нему трижды в шаблоне не даст трёх SQL-запросов.
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); // с НДС
}
}<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. Это отличает его от обычных публичных
свойств.
Кэширование между запросами
// Кэш на 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();
}Сброс кэша
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-методах, и в шаблоне, и
кэшируется.
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] синхронизирует свойство со строкой запроса.
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
Хранение в сессии
use Livewire\Attributes\Session;
class Sidebar extends Component
{
#[Session]
public bool $collapsed = false;
#[Session(key: 'admin.table.density')]
public string $density = 'comfortable';
}Ручная работа с сессией и flash
public function save(): void
{
$this->validate();
Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);
session()->flash('status', 'Настройки сохранены');
}@if (session('status'))
<div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif#[Url]-свойство при изменении
дёргает History API. Для полей, которые меняются на каждое нажатие клавиши, комбинируйте с
wire:model.live.debounce.
14. Загрузка файлов
Трейт WithFileUploads добавляет полноценную загрузку: файл уходит на сервер сразу после
выбора, кладётся во временное хранилище и становится доступен как объект
TemporaryUploadedFile.
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);
}
}<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>Точный индикатор прогресса
<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>Настройка временного хранилища
'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, // минут до автоочистки
],upload_max_filesize, post_max_size и
max_execution_time в php.ini, а также client_max_body_size в
nginx должны быть не меньше вашего max: в правилах. Иначе загрузка обрывается без
внятной ошибки.
15. Пагинация
Трейт WithPagination подключает пагинацию Laravel без перезагрузки страницы.
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),
]);
}
}<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>Полезные детали
// Своё имя параметра страницы — нужно, если на странице два пагинатора
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.
{{-- Показать во время любого запроса компонента --}}
<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>Модификаторы
{{-- Заблокировать кнопку --}}
<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
<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
<div wire:offline class="banner banner--warn">
Нет соединения с сервером. Изменения не сохраняются.
</div>Скелетон при первой загрузке
<div wire:init="loadHeavyData">
@if ($loaded)
{{-- реальные данные --}}
@else
<div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
@endif
</div>18. Ленивая загрузка и polling
Ленивая загрузка компонента
Тяжёлый компонент можно не рендерить в первом ответе: страница отдаётся мгновенно с заглушкой, а содержимое подгружается вторым запросом.
<livewire:revenue-chart lazy />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" управляют моментом загрузки.
{{-- Подгрузить, когда блок попадёт во вьюпорт --}}
<livewire:revenue-chart lazy="on-scroll" />Отложенная инициализация: wire:init
<div wire:init="loadStats">
@if ($stats)
…
@else
<div class="skeleton"></div>
@endif
</div>Опрос сервера: wire:poll
{{-- Обновлять каждые 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.
<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
<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>Собственные скрипты внутри компонента
@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{{-- Загрузить внешнюю библиотеку один раз на всю страницу --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassetsИгнорирование поддерева
Если сторонний виджет сам управляет DOM, morph-алгоритм ему только мешает.
<div wire:ignore>
<select id="select2-field">…</select>
</div>
{{-- Игнорировать только детей, но обновлять атрибуты самого элемента --}}
<div wire:ignore.self>…</div>Глобальные JS-хуки
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.
<?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);
});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) | Размер массива/коллекции |
Аутентификация, параметры, события
// От имени пользователя
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. Безопасность
1. Авторизация внутри каждого действия
public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post); // Policy — обязательно
$post->delete();
}// Авторизация всего компонента
public function mount(Project $project): void
{
$this->authorize('view', $project);
$this->project = $project;
}2. #[Locked] для идентификаторов
use Livewire\Attributes\Locked;
class InvoiceEditor extends Component
{
#[Locked]
public int $invoiceId; // подменить с фронтенда нельзя
public string $note = '';
}Без #[Locked] достаточно одной строчки в консоли браузера:
Livewire.find('...').set('invoiceId', 999) // чужой счёт3. Правила для привязки к моделям
wire:model="post.title" работает, только если поле разрешено правилом валидации —
так Livewire защищает от массового присвоения.
protected function rules(): array
{
return [
'post.title' => 'required|string|max:180',
'post.body' => 'required|string',
// 'post.user_id' сознательно НЕ включаем — иначе пост можно переписать на другого автора
];
}4. Экранирование вывода
{{-- Безопасно: экранируется --}}
{{ $comment->body }}
{{-- Опасно: сырой HTML от пользователя --}}
{!! $comment->body !!}
{{-- Если HTML нужен — санитизируйте на сервере --}}
{!! clean($comment->body) !!}5. Ограничение частоты
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. Не храните секреты в свойствах
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. Не гоняйте запросы на каждое нажатие клавиши
{{-- Плохо: запрос на каждый символ --}}
<input wire:model.live="search">
{{-- Хорошо --}}
<input wire:model.live.debounce.400ms="search">3. Дробите тяжёлые страницы на компоненты
Обновление вложенного компонента не перерендеривает родителя. Дашборд из шести независимых виджетов работает заметно живее, чем один монолит.
4. Кэшируйте дорогие вычисления
#[Computed(persist: true, seconds: 600)]
public function monthlyRevenue(): array
{
return app(RevenueService::class)->byMonth();
}5. Следите за N+1
// Плохо — запрос на каждую строку в шаблоне
$orders = Order::paginate(50);
// Хорошо
$orders = Order::with(['customer', 'items.product'])->paginate(50);Включите строгий режим в разработке, чтобы N+1 падал с ошибкой:
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}6. wire:key в списках
Без ключей morph-алгоритм перестраивает больше узлов, чем нужно, — и делает это неверно.
7. Ленивая загрузка тяжёлых блоков
<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 в цикле. Ключ должен быть стабильным и уникальным.
@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 и
синхронизируйте вручную.
<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, а не полагайтесь на перерисовку.
$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:navigate | SPA-переход по ссылке |
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] | Метод без перерисовки |
Полезные методы компонента
$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-команды
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Официальные ресурсы
- Документация Livewire: livewire.laravel.com/docs
- Документация Laravel: laravel.com/docs
- Alpine.js: alpinejs.dev