Перейти к содержанию

Действия⚓︎

Действия Livewire — это методы вашего компонента, которые могут быть вызваны при взаимодействии с фронтендом, например, при клике на кнопку или отправке формы. Они дают разработчику ощущение, будто он может напрямую вызывать метод PHP из браузера, позволяя сосредоточиться на логике приложения, не тратя время на написание шаблонного кода для связи фронтенда и бэкенда.

Давайте рассмотрим простой пример вызова действия save:

resources/views/components/post/⚡create.blade.php
<?php

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

new class extends Component {
    public $title = '';

    public $content = '';

    public function save()
    {
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
        ]);

        return redirect()->to('/posts');
    }
};
?>

<form wire:submit="save"> <!-- [tl! highlight] -->
    <input type="text" wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Сохранить</button>
</form>

В приведённом выше примере, когда пользователь отправляет форму, нажимая «Сохранить», директива wire:submit перехватывает событие submit и вызывает действие save() на сервере.

По сути, действия — это удобный способ связать действия пользователя с серверной логикой без необходимости вручную отправлять и обрабатывать AJAX-запросы.

Передача параметров⚓︎

Livewire позволяет передавать параметры из Blade-шаблона в методы-действия вашего компонента. Это даёт возможность передать действию дополнительные данные или состояние с фронтенда в момент его вызова.

Например, представим, что у вас есть компонент ShowPosts, который позволяет пользователям удалять посты. Вы можете передать ID поста как параметр в действие delete() вашего Livewire-компонента. Тогда метод сможет найти нужный пост и удалить его из базы данных:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::findOrFail($id);

        $this->authorize('delete', $post);

        $post->delete();
    }
};
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Удалить</button>
        </div>
    @endforeach
</div>

Для поста с ID равным 2 кнопка «Удалить» в приведённом выше Blade-шаблоне будет отображаться в браузере как:

<button wire:click="delete(2)">Удалить</button>

Когда эта кнопка будет нажата, метод delete() будет вызван, а параметр $id получит значение «2».

Не доверяйте параметрам действий

Параметры действий следует рассматривать так же, как входные данные HTTP-запроса, то есть значения параметров действий нельзя считать доверенными. Вы всегда должны проверять право владения сущностью перед её обновлением в базе данных.

Более подробную информацию смотрите в нашей документации по вопросам безопасности и лучшим практикам.

Для дополнительного удобства вы можете автоматически получать модели Eloquent по соответствующему ID модели, переданному в действие в качестве параметра. Это очень похоже на привязку модели в маршрутах. Чтобы начать использовать эту возможность, достаточно указать в типизации параметра действия класс модели — тогда нужная модель будет автоматически получена из базы данных и передана в действие вместо самого ID:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();
    }
};

Внедрение зависимостей⚓︎

Вы можете воспользоваться системой внедрения зависимостей Laravel, указывая типы параметров в сигнатуре вашего действия. Livewire и Laravel автоматически разрешат зависимости действия из контейнера:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use App\Repositories\PostRepository;
use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete(PostRepository $posts, $postId)
    {
        $posts->deletePost($postId);
    }
};
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Удалить</button>
        </div>
    @endforeach
</div>

В этом примере метод delete() сначала получает экземпляр PostRepository, разрешённый через контейнер сервисов Laravel, а уже затем получает переданный параметр $postId.

Слушатели событий⚓︎

Livewire поддерживает множество различных слушателей событий, позволяя реагировать на самые разные виды взаимодействия пользователя:

Слушатель Описание
wire:click Срабатывает при клике на элемент
wire:submit Срабатывает при отправке формы
wire:keydown Срабатывает при нажатии клавиши
wire:keyup Срабатывает при отпускании клавиши
wire:mouseenter Срабатывает при наведении мыши на элемент
wire:* Всё, что следует после wire:, становится именем события слушателя

Поскольку после wire: можно указать любое имя, Livewire позволяет подписываться практически на любое событие браузера, которое вам может понадобиться. Например, чтобы отловить событие transitionend, можно использовать wire:transitionend.

Прослушивание конкретных клавиш⚓︎

Livewire предоставляет удобные псевдонимы, которые позволяют сузить слушатели событий нажатия клавиш до конкретной клавиши или комбинации клавиш.

Например, чтобы выполнить поиск, когда пользователь нажимает Enter после ввода текста в поле поиска, можно использовать wire:keydown.enter:

<input wire:model="query" wire:keydown.enter="searchPosts">

Вы можете последовательно добавлять дополнительные псевдонимы клавиш после первого, чтобы отслеживать комбинации клавиш. Например, если вы хотите реагировать на клавишу Enter только при зажатой клавише Shift, можно написать так:

<input wire:keydown.shift.enter="...">

Ниже приведён список всех доступных модификаторов клавиш:

Модификатор Клавиша
.shift Shift
.enter Enter
.space Пробел (Space)
.ctrl Ctrl
.cmd Cmd
.meta Cmd на Mac, клавиша Windows на Windows
.alt Alt
.up Стрелка вверх
.down Стрелка вниз
.left Стрелка влево
.right Стрелка вправо
.escape Escape
.tab Tab
.caps-lock Caps Lock
.equal Знак равенства =
.period Точка .
.slash Слеш /

Модификаторы обработчиков событий⚓︎

Livewire также предоставляет удобные модификаторы, которые делают типичные задачи обработки событий очень простыми.

Например, если вам нужно вызвать event.preventDefault() внутри слушателя события, достаточно добавить к имени события суффикс .prevent:

<input wire:keydown.prevent="...">

Вот полный список всех доступных модификаторов слушателей событий и их функций:

Модификатор Описание
.prevent Эквивалент вызова .preventDefault()
.stop Эквивалент вызова .stopPropagation()
.window Слушает событие на объекте window
.outside Слушает клики только «снаружи» элемента
.document Слушает события на объекте document
.once Гарантирует, что слушатель будет вызван только один раз
.debounce Задерживает выполнение обработчика на 250 мс по умолчанию
.debounce.100ms Задерживает выполнение обработчика на указанное время
.throttle Ограничивает вызов обработчика не чаще, чем раз в 250 мс
.throttle.100ms Ограничивает вызов обработчика с пользовательской длительностью
.self Вызывает слушатель только если событие возникло именно на этом элементе, а не на его дочерних
.camel Преобразует имя события в camelCase (wire:custom-eventcustomEvent)
.dot Преобразует имя события в точечную нотацию (wire:custom-eventcustom.event)
.passive wire:touchstart.passive не блокирует производительность прокрутки
.capture Слушает событие на фазе «захвата» (capturing phase)

Поскольку wire: использует под капотом директиву x-on от Alpine, все эти модификаторы доступны благодаря Alpine. Чтобы лучше понять, когда и как их использовать, смотрите документацию по событиям в Alpine.

Обработка событий от сторонних библиотек⚓︎

Livewire также позволяет слушать пользовательские события, которые генерируют сторонние библиотеки.

Например, представим, что вы используете WYSIWYG-редактор Trix в своём проекте и хотите реагировать на событие trix-change, чтобы получать содержимое редактора. Это можно сделать с помощью директивы wire:trix-change:

<form wire:submit="save">
    <!-- ... -->

    <trix-editor
        wire:trix-change="setPostContent($event.target.value)"
    ></trix-editor>

    <!-- ... -->
</form>

В этом примере действие setPostContent вызывается каждый раз, когда срабатывает событие trix-change, обновляя свойство content в Livewire-компоненте текущим значением редактора Trix.

Доступ к объекту события через $event

Внутри обработчиков событий Livewire вы можете получить доступ к объекту события с помощью $event. Это удобно, когда нужно получить информацию о событии. Например, можно обратиться к элементу, который вызвал событие, через $event.target.

Предупреждение

Приведённый выше демонстрационный код с Trix является неполным и служит только для демонстрации работы слушателей событий. Если использовать его буквально, сетевой запрос будет отправляться при каждом нажатии клавиши. Более производительная реализация будет выглядеть так:

<trix-editor
    x-on:trix-change="$wire.content = $event.target.value"
></trix-editor>

Прослушивание отправленных пользовательских событий⚓︎

Если ваше приложение отправляет пользовательские события из Alpine, вы также можете подписываться на них с помощью Livewire:

<div wire:custom-event="...">

    <!-- Глубоко вложенный внутри этого компонента: -->
    <button x-on:click="$dispatch('custom-event')">...</button>

</div>

Когда в приведённом выше примере нажимается кнопка, событие custom-event отправляется (dispatched) и всплывает вверх до корня Livewire-компонента, где директива wire:custom-event его перехватывает и вызывает указанное действие.

Если вы хотите отловить событие, которое отправляется где-то в другом месте вашего приложения, вам нужно дождаться, пока оно всплывёт до объекта window, и прослушивать его уже там. К счастью, Livewire делает это очень просто — достаточно добавить модификатор .window к любому слушателю событий:

<div wire:custom-event.window="...">
    <!-- ... -->
</div>

<!-- Отправлено где-то на странице вне компонента: -->
<button x-on:click="$dispatch('custom-event')">...</button>

Отключение полей ввода во время отправки формы⚓︎

Рассмотрим пример CreatePost, который мы обсуждали ранее:

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

    <textarea wire:model="content"></textarea>

    <button type="submit">Сохранить</button>
</form>

Когда пользователь нажимает «Сохранить», отправляется сетевой запрос на сервер для вызова действия save() в Livewire-компоненте.

Но представим, что пользователь заполняет эту форму при медленном интернет-соединении. Он нажимает «Сохранить», и сначала ничего не происходит, потому что запрос занимает больше времени, чем обычно. Пользователь может подумать, что отправка не удалась, и попытаться нажать кнопку «Сохранить» ещё раз, пока первый запрос ещё обрабатывается.

В таком случае одновременно будут обрабатываться два запроса на одно и то же действие.

Чтобы предотвратить подобную ситуацию, Livewire автоматически отключает кнопку отправки и все поля ввода внутри элемента <form>, пока выполняется действие, привязанное через wire:submit. Это гарантирует, что форма не будет случайно отправлена дважды.

Чтобы ещё больше снизить путаницу у пользователей с медленным соединением, часто полезно показывать какой-нибудь индикатор загрузки — например, лёгкое изменение цвета фона или анимацию SVG.

Livewire предоставляет директиву wire:loading, которая делает показ и скрытие индикаторов загрузки в любом месте страницы очень простым. Вот короткий пример использования wire:loading для отображения сообщения о загрузке под кнопкой «Сохранить»:

<form wire:submit="save">
    <textarea wire:model="content"></textarea>

    <button type="submit">Сохранить</button>

    <span wire:loading>Сохранение...</span>
</form>

Альтернативно, вы можете стилизовать состояния загрузки напрямую с помощью Tailwind и автоматического атрибута data-loading, который добавляет Livewire:

<form wire:submit="save">
    <textarea wire:model="content"></textarea>

    <button type="submit" class="data-loading:opacity-50">Сохранить</button>

    <span class="not-data-loading:hidden">Сохранение...</span>
</form>

В большинстве случаев использование селекторов data-loading проще и гибче, чем директива wire:loading. Подробнее о состояниях загрузки →

Обновление компонента⚓︎

Иногда вам может понадобиться просто «обновить» компонент. Например, если у вас есть компонент, который проверяет статус чего-либо в базе данных, вы можете добавить кнопку, позволяющую пользователям вручную обновить отображаемые результаты.

Это можно сделать с помощью простого действия $refresh в Livewire — в любом месте, где вы обычно обращаетесь к методу своего компонента:

<button type="button" wire:click="$refresh">...</button>

Когда действие $refresh срабатывает, Livewire выполнит полный круг обращения к серверу и заново отрендерит ваш компонент, не вызывая при этом никаких методов.

Важно отметить, что все ожидающие обновления данных в компоненте (например, привязки через wire:model) будут применены на сервере в момент обновления компонента.

Вы также можете запустить обновление компонента с помощью Alpine.js прямо внутри вашего Livewire-компонента:

<button type="button" x-on:click="$wire.$refresh()">...</button>

Узнайте больше, прочитав документацию по использованию Alpine внутри Livewire.

Подтверждение действия⚓︎

Когда вы разрешаете пользователям выполнять опасные действия — например, удалять пост из базы данных, — стоит показывать им окно подтверждения, чтобы убедиться, что они действительно хотят это сделать.

Livewire делает это очень просто благодаря директиве wire:confirm:

<button
    type="button"
    wire:click="delete"
    wire:confirm="Хотите удалить этот пост?"
>
    Удалить пост
</button>

Когда к элементу, содержащему действие Livewire, добавляется wire:confirm, при попытке пользователя запустить это действие ему будет показано диалоговое окно подтверждения с указанным сообщением. Пользователь может нажать «OK», чтобы подтвердить действие, либо «Отмена» или клавишу Escape, чтобы отказаться.

Подробную информацию смотрите на странице документации wire:confirm.

Вызов действий из Alpine⚓︎

Livewire отлично интегрируется с Alpine. Более того, под капотом каждый Livewire-компонент одновременно является и Alpine-компонентом. Это значит, что вы можете в полной мере использовать возможности Alpine внутри своих компонентов, добавляя клиентскую интерактивность на JavaScript.

Чтобы сделать эту связку ещё мощнее, Livewire предоставляет магический объект $wire в Alpine, который можно рассматривать как JavaScript-представление вашего PHP-компонента. Помимо доступа и изменения публичных свойств через $wire, вы можете вызывать действия. Когда действие вызывается на объекте $wire, соответствующий метод PHP будет выполнен на вашем серверном Livewire-компоненте:

<button x-on:click="$wire.save()">Сохранить пост</button>

Или, чтобы показать более сложный пример, вы можете использовать утилиту x-intersect от Alpine, чтобы запускать действие Livewire incrementViewCount() в момент, когда определённый элемент становится видимым на странице:

<div x-intersect="$wire.incrementViewCount()">...</div>

Передача параметров⚓︎

Любые параметры, которые вы передаёте методу $wire, также будут переданы методу PHP-класса. Например, рассмотрим следующее действие Livewire:

<?php

public function addTodo($todo)
{
    $this->todos[] = $todo;
}

Внутри Blade-шаблона вашего компонента вы можете вызвать это действие через Alpine, передав параметр, который должен быть передан в действие:

<div x-data="{ todo: '' }">
    <input type="text" x-model="todo">

    <button x-on:click="$wire.addTodo(todo)">Добавить пункт</button>
</div>

Если пользователь ввёл в текстовое поле «Вынести мусор» и нажал кнопку «Добавить пункт», то будет вызван метод addTodo() с параметром $todo, значение которого будет равно «Вынести мусор».

Получение возвращаемых значений⚓︎

Для ещё большей гибкости вызванные через $wire действия возвращают promise, пока идёт сетевой запрос. Когда сервер вернёт ответ, promise разрешится значением, которое вернул серверный метод действия.

Например, рассмотрим Livewire-компонент со следующим действием:

<?php

use App\Models\Post;

public function getPostCount()
{
    return Post::count();
}

Используя $wire, действие можно вызвать, а его возвращаемое значение — получить (разрешить promise):

<span x-init="$el.innerHTML = await $wire.getPostCount()"></span>

В этом примере, если метод getPostCount() вернёт значение «10», то тег <span> также будет содержать «10».

Используйте атрибут #[Json] для действий, потребляемых JavaScript

Для действий, которые в основном используются JavaScript, рекомендуется применять атрибут #[Json]. Он возвращает данные через разрешение/отклонение promise, автоматически обрабатывает ошибки валидации через отклонение promise и пропускает повторный рендеринг для повышения производительности.

Знание Alpine не требуется для работы с Livewire; однако это чрезвычайно мощный инструмент, и его освоение значительно улучшит ваш опыт работы с Livewire и повысит продуктивность.

JavaScript-действия⚓︎

Livewire позволяет определять действия на JavaScript, которые выполняются полностью на стороне клиента без обращения к серверу. Это полезно в двух случаях:

  1. Когда нужно выполнить простые обновления интерфейса, не требующие связи с сервером
  2. Когда вы хотите оптимистично обновить интерфейс с помощью JavaScript до выполнения серверного запроса

Чтобы определить JavaScript-действие, можно использовать функцию $js() внутри тега <script> в вашем компоненте.

Вот пример добавления поста в закладки, который использует JavaScript-действие для оптимистичного обновления интерфейса до отправки запроса на сервер. JavaScript-действие сразу показывает заполненную иконку закладки, а затем отправляет запрос на сохранение закладки в базе данных:

resources/views/components/post/⚡show.blade.php
<?php

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

new class extends Component {
    public Post $post;

    public $bookmarked = false;

    public function mount()
    {
        $this->bookmarked = $this->post->bookmarkedBy(auth()->user());
    }

    public function bookmarkPost()
    {
        $this->post->bookmark(auth()->user());

        $this->bookmarked = $this->post->bookmarkedBy(auth()->user());
    }
};
<div>
    <button wire:click="$js.bookmark" class="flex items-center gap-1">
        { Outlined bookmark icon... }
        <svg wire:show="!bookmarked" wire:cloak xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" />
        </svg>

        { Solid bookmark icon... }
        <svg wire:show="bookmarked" wire:cloak xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
            <path fill-rule="evenodd" d="M6.32 2.577a49.255 49.255 0 0 1 11.36 0c1.497.174 2.57 1.46 2.57 2.93V21a.75.75 0 0 1-1.085.67L12 18.089l-7.165 3.583A.75.75 0 0 1 3.75 21V5.507c0-1.47 1.073-2.756 2.57-2.93Z" clip-rule="evenodd" />
        </svg>
    </button>
</div>

<script>
    this.$js.bookmark = () => {
        $wire.bookmarked = !$wire.bookmarked

        $wire.bookmarkPost()
    }
</script>

Когда пользователь нажимает на кнопку с сердечком, происходит следующая последовательность:

  1. Запускается JavaScript-действие «bookmark»
  2. Иконка сердечка мгновенно обновляется путём переключения значения $wire.bookmarked на стороне клиента
  3. Вызывается метод bookmarkPost(), чтобы сохранить изменение в базе данных

Это обеспечивает мгновенную визуальную обратную связь, одновременно гарантируя, что состояние закладки корректно сохраняется.

Для классовых компонентов нужен обёрточный @script

Приведённые выше примеры используют обычные теги <script>, что работает для однофайловых и многофайловых компонентов. Если вы используете классовые компоненты, необходимо обернуть теги script директивой @script:

@script
<script>
    this.$js.bookmark = () => { /* ... */ }
</script>
@endscript
Это гарантирует, что ваш JavaScript будет правильно привязан к данному компоненту.

Вызов из Alpine⚓︎

Вы можете напрямую вызывать JavaScript-действия из Alpine, используя объект $wire. Например, можно вызвать JavaScript-действие bookmark через объект $wire:

<button x-on:click="$wire.$js.bookmark()">Закладка</button>

Вызов из PHP⚓︎

JavaScript-действия также можно вызывать из PHP с помощью метода js():

resources/views/components/post/⚡create.blade.php
<?php

use Livewire\Component;

new class extends Component {
    public $title = '';

    public function save()
    {
        // ...

        $this->js('onPostSaved');
    }
};
<div>
    <!-- ... -->

    <button wire:click="save">Сохранить</button>
</div>

<script>
    this.$js.onPostSaved = () => {
        alert('Ваш пост успешно сохранён!')
    }
</script>

В этом примере, когда действие save() завершится, будет выполнено JavaScript-действие postSaved, которое вызовет диалоговое окно с оповещением.

Магические действия⚓︎

Livewire предоставляет набор «магических» действий, которые позволяют выполнять распространённые задачи в компонентах без необходимости определять собственные методы. Эти магические действия можно использовать внутри слушателей событий, определённых в ваших Blade-шаблонах.

$parent⚓︎

Магическая переменная $parent позволяет обращаться к свойствам родительского компонента и вызывать действия родительского компонента из дочернего компонента:

<button wire:click="$parent.removePost({{ $post->id }})">Удалить</button>

В приведённом выше примере, если родительский компонент имеет действие removePost(), дочерний компонент может вызвать его напрямую из своего Blade-шаблона, используя $parent.removePost().

$set⚓︎

Магическое действие $set позволяет обновлять свойство вашего Livewire-компонента прямо из Blade-шаблона. Чтобы воспользоваться $set, укажите свойство, которое нужно обновить, и новое значение в качестве аргументов:

<button wire:click="$set('query', '')">Сбросить поиск</button>

В этом примере, когда нажимается кнопка, отправляется сетевой запрос, который устанавливает свойство $query в компоненте в значение ''.

$refresh⚓︎

Действие $refresh запускает повторный рендеринг вашего Livewire-компонента. Это полезно, когда нужно обновить отображение компонента, не изменяя при этом значения каких-либо свойств:

<button wire:click="$refresh">Обновить</button>

Когда кнопка будет нажата, компонент перерендерится, что позволит увидеть последние изменения в представлении.

$toggle⚓︎

Действие $toggle используется для переключения (инвертирования) значения булевого свойства в вашем Livewire-компоненте:

<button wire:click="$toggle('sortAsc')">
    Сортировать {{ $sortAsc ? 'по убыванию' : 'по возрастанию' }}
</button>

В этом примере при нажатии на кнопку свойство $sortAsc в компоненте будет переключаться между значениями true и false.

$dispatch⚓︎

Действие $dispatch позволяет отправить событие Livewire прямо в браузере. Ниже приведён пример кнопки, которая при нажатии отправит событие post-deleted:

<button type="submit" wire:click="$dispatch('post-deleted')">Удалить пост</button>

$event⚓︎

Действие $event может использоваться внутри слушателей событий, таких как wire:click. Это действие даёт доступ к реальному JavaScript-событию, которое было вызвано, позволяя обращаться к элементу, вызвавшему событие, и другой связанной информации:

<input type="text" wire:keydown.enter="search($event.target.value)">

Когда пользователь нажимает клавишу Enter во время ввода текста в поле выше, содержимое этого поля будет передано как параметр в действие search().

Вызов магических действий из Alpine⚓︎

Вы также можете вызывать магические действия из Alpine с помощью объекта $wire. Например, можно использовать объект $wire, чтобы запустить магическое действие $refresh:

<button x-on:click="$wire.$refresh()">Обновить</button>

Пропуск повторного рендеринга⚓︎

Иногда в вашем компоненте может быть действие, которое не имеет побочных эффектов, способных изменить отрендеренный Blade-шаблон при его вызове. В таких случаях вы можете пропустить этап render в жизненном цикле Livewire, добавив атрибут #[Renderless] над методом действия.

Для примера: в компоненте ShowPost ниже количество просмотров логируется, когда пользователь прокрутил пост до конца:

resources/views/components/post/⚡show.blade.php
<?php

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

new class extends Component {
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    #[Renderless]
    public function incrementViewCount()
    {
        $this->post->incrementViewCount();
    }
};
<div>
    <h1>{{ $post->title }}</h1>
    <p>{{ $post->content }}</p>

    <div wire:intersect="incrementViewCount"></div>
</div>

Приведённый выше пример использует wire:intersect, чтобы вызвать действие в момент, когда элемент попадает в область видимости (обычно применяется для определения, когда пользователь прокрутил страницу до элемента, расположенного ниже).

Как видно, когда пользователь прокручивает до конца поста, вызывается метод incrementViewCount(). Поскольку к действию добавлен атрибут #[Renderless], просмотр логируется, но шаблон не перерендеривается и никакая часть страницы не изменяется.

Если вы предпочитаете не использовать атрибуты методов или вам нужно пропускать рендеринг условно, вы можете вызвать метод skipRender() внутри действия вашего компонента:

resources/views/components/post/⚡show.blade.php
<?php

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

new class extends Component {
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    public function incrementViewCount()
    {
        $this->post->incrementViewCount();

        $this->skipRender();
    }
};

Вы также можете пропустить рендеринг напрямую из элемента, используя модификатор .renderless:

<button type="button" wire:click.renderless="incrementViewCount">

Параллельное выполнение с async⚓︎

По умолчанию Livewire выполняет действия внутри одного компонента последовательно (сериализует их), чтобы гарантировать предсказуемые обновления состояния. Если одно действие уже отправлено и находится в процессе, последующие действия ставятся в очередь и ждут его завершения. Это предотвращает состояния гонки и сохраняет консистентность состояния компонента. Однако иногда требуется, чтобы действия выполнялись немедленно, параллельно, а не по очереди.

Атрибут #[Async] и модификатор wire:click.async указывают Livewire выполнять действие параллельно, обходя обычную очередь запросов.

Использование модификатора async⚓︎

Любое действие можно сделать асинхронным, добавив модификатор .async к слушателю события:

<button wire:click.async="logActivity">Отслеживать событие</button>

При нажатии на эту кнопку действие logActivity сработает немедленно, даже если другие запросы находятся в обработке. Оно не заблокирует последующие запросы, и другие запросы не заблокируют его.

Использование атрибута Async⚓︎

Альтернативный способ — пометить метод как асинхронный, используя атрибут #[Async]. Это сделает действие асинхронным независимо от того, откуда оно вызвано:

resources/views/components/post/⚡show.blade.php
<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public Post $post;

    #[Async]
    public function logActivity()
    {
        Activity::log('post-viewed', $this->post);
    }

    // ...
};
<div wire:intersect="logActivity">
    <!-- ... -->
</div>

В этом примере, когда элемент попадает в область видимости, logActivity() вызывается асинхронно, не блокируя другие выполняющиеся запросы.

Когда использовать асинхронные действия⚓︎

Асинхронные действия полезны для операций типа "запустил и забыл" (fire-and-forget), где результат не влияет на то, что отображается на странице. Распространенные сценарии использования включают:

  • Аналитика и логирование: Отслеживание поведения пользователей, просмотров страниц или взаимодействий
  • Фоновые операции: Запуск задач (jobs), отправка уведомлений или обновление внешних сервисов
  • Результаты только для JavaScript: Получение данных через await $wire.getData(), которые будут использованы исключительно в JavaScript

Вот пример отслеживания клика пользователя по внешней ссылке:

<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public $url;

    #[Async]
    public function trackClick()
    {
        Analytics::track('external-link-clicked', [
            'url' => $this->url,
            'user_id' => auth()->id(),
        ]);
    }

    // ...
};
<a href="{{ $url }}" target="_blank" wire:click.async="trackClick">
    Посетить внешний сайт
</a>

Поскольку отслеживание происходит асинхронно, клик пользователя не задерживается сетевым запросом.

Когда НЕЛЬЗЯ использовать асинхронные действия⚓︎

Асинхронные действия и мутации состояния несовместимы

Никогда не используйте асинхронные действия, если они изменяют состояние компонента, отображаемое в UI. Поскольку асинхронные действия выполняются параллельно, это может привести к непредсказуемым состояниям гонки, когда состояние компонента расходится в нескольких одновременных запросах.

Рассмотрим этот опасный пример:

resources/views/components/⚡counter.blade.php
// Внимание: Этот фрагмент демонстрирует, что НЕ СЛЕДУЕТ делать...

<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    public $count = 0;

    #[Async] // Не делайте этого!
    public function increment()
    {
        $this->count++; // Мутация состояния в асинхронном действии
    }

    // ...
};

Если пользователь быстро нажимает кнопку увеличения, несколько асинхронных запросов будут запущены одновременно. Каждый запрос начнется с одного и того же начального значения $count, что приведет к потере обновлений. Вы можете нажать 5 раз, но увидите, что счетчик увеличился только на 1.

Золотое правило: Используйте async только для действий, которые выполняют побочные эффекты — операций, которые не изменяют никаких свойств, влияющих на представление вашего компонента.

Получение данных для JavaScript⚓︎

Еще один допустимый сценарий использования — получение данных с сервера, которые будут полностью обработаны в JavaScript, не затрагивая состояние отрисовки вашего компонента:

<?php

use Livewire\Attributes\Async;
use Livewire\Component;

new class extends Component {
    #[Async]
    public function fetchSuggestions($query)
    {
        return Post::where('title', 'like', "%{$query}%")
            ->limit(5)
            ->pluck('title');
    }

    // ...
};
<div x-data="{ suggestions: [] }">
    <input
        type="text"
        x-on:input.debounce="suggestions = await $wire.fetchSuggestions($event.target.value)"
    >

    <template x-for="suggestion in suggestions">
        <div x-text="suggestion"></div>
    </template>
</div>

Поскольку подсказки хранятся исключительно в данных Alpine suggestions и никогда не попадают в состояние компонента Livewire, их безопасно получать асинхронно.

Сохранение позиции прокрутки⚓︎

При обновлении контента браузер может перескочить на другую позицию прокрутки. Модификатор .preserve-scroll сохраняет текущую позицию прокрутки во время обновлений:

<button wire:click.preserve-scroll="loadMore">Загрузить ещё</button>

<select wire:model.live.preserve-scroll="category">...</select>

Это полезно для бесконечной прокрутки, фильтров и динамического обновления контента, когда вы не хотите, чтобы страница «прыгала».

Вопросы безопасности⚓︎

Помните, что любой публичный метод вашего Livewire-компонента может быть вызван с клиентской стороны, даже если для него нет соответствующего обработчика wire:click. В таких случаях пользователи всё равно могут запустить действие через DevTools браузера.

Ниже приведены три примера уязвимостей в Livewire-компонентах, которые легко пропустить. В каждом случае сначала показана уязвимая версия компонента, а затем — безопасная. В качестве упражнения попробуйте самостоятельно найти уязвимости в первом примере, прежде чем смотреть решение.

Если вам сложно заметить уязвимости и это вызывает беспокойство по поводу способности защитить собственные приложения — помните, что все эти уязвимости точно так же применимы к обычным веб-приложениям, использующим запросы и контроллеры. Если вы используете метод компонента как прокси для метода контроллера, а его параметры — как прокси для входных данных запроса, то сможете применить уже имеющиеся у вас знания о безопасности веб-приложений и к коду на Livewire.

Всегда авторизуйте параметры действий⚓︎

Как и в случае с входными данными запроса в контроллерах, крайне важно авторизовать параметры действий, поскольку это произвольные данные, введённые пользователем.

Ниже приведён компонент ShowPosts, в котором пользователи могут просматривать все свои посты на одной странице. Они могут удалить любой пост, используя одну из кнопок «Удалить» у поста.

Вот уязвимая версия компонента:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::find($id);

        $post->delete();
    }
};
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Удалить</button>
        </div>
    @endforeach
</div>

Помните, что злоумышленник может вызвать delete() прямо из JavaScript-консоли, передав в действие любые параметры, какие захочет. Это означает, что пользователь, просматривающий один из своих постов, может удалить пост другого пользователя, передав ID чужого поста в метод delete().

Чтобы защититься от этого, нам нужно авторизовать пользователя, убедившись, что он владеет постом, который собирается удалить:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function delete($id)
    {
        $post = Post::find($id);

        $this->authorize('delete', $post);

        $post->delete();
    }
};

Всегда авторизуйте на стороне сервера⚓︎

Как и стандартные контроллеры Laravel, действия Livewire могут быть вызваны любым пользователем, даже если в пользовательском интерфейсе нет возможности вызвать это действие.

Рассмотрим следующий компонент BrowsePosts, где любой пользователь может просматривать все посты в приложении, но удалять посты могут только администраторы:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        $post = Post::find($id);

        $post->delete();
    }
};
<div>
    @foreach ($this->posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            @if (Auth::user()->isAdmin())
                <button wire:click="deletePost({{ $post->id }})">Удалить</button>
            @endif
        </div>
    @endforeach
</div>

Как видите, только администраторы могут видеть кнопку «Удалить»; однако любой пользователь может вызвать deletePost() в компоненте через инструменты разработчика браузера.

Чтобы закрыть эту уязвимость, нам нужно авторизовать действие на сервере следующим образом:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $post = Post::find($id);

        $post->delete();
    }
};

С этим изменением только администраторы могут удалить пост из этого компонента.

Держите опасные методы защищенными или приватными⚓︎

Каждый публичный метод внутри вашего компонента Livewire может быть вызван с клиента. Даже те методы, на которые вы не ссылались внутри обработчика wire:click. Чтобы предотвратить вызов пользователем метода, который не предназначен для вызова с клиентской стороны, вы должны пометить их как protected или private. Сделав это, вы ограничите видимость этого чувствительного метода классом компонента и его подклассами, гарантируя, что они не могут быть вызваны с клиентской стороны.

Рассмотрим пример BrowsePosts, который мы обсуждали ранее, где пользователи могут просматривать все посты в вашем приложении, но удалять посты могут только администраторы. В разделе Всегда авторизуйте на стороне сервера мы сделали действие безопасным, добавив серверную авторизацию. Теперь представьте, что мы рефакторим фактическое удаление поста в отдельный метод, как вы могли бы сделать для упрощения кода:

resources/views/components/post/⚡index.blade.php
// Внимание: Этот фрагмент демонстрирует, что НЕ СЛЕДУЕТ делать...
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id);
    }

    public function delete($postId)
    {
        $post = Post::find($postId);

        $post->delete();
    }
};
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="deletePost({{ $post->id }})">Удалить</button>
        </div>
    @endforeach
</div>

Как видите, мы отрефакторили логику удаления поста в отдельный метод под названием delete(). Даже если этот метод нигде не используется в нашем шаблоне, если пользователь узнает о его существовании, он сможет вызвать его через инструменты разработчика браузера, так как он public.

Чтобы исправить это, мы можем пометить метод как protected или private. Как только метод будет помечен как protected или private, при попытке пользователя вызвать его будет выброшена ошибка:

resources/views/components/post/⚡index.blade.php
<?php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    #[Computed]
    public function posts()
    {
        return Auth::user()->posts;
    }

    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id);
    }

    protected function delete($postId)
    {
        $post = Post::find($postId);

        $post->delete();
    }
};

Смотрите также⚓︎

  • События — Обмен данными между компонентами с помощью событий
  • Формы — Обработка отправки форм с помощью действий
  • Состояния загрузки — Отображение обратной связи во время выполнения действий
  • wire:click — Запуск действий по клику на кнопку
  • Валидация — Проверка данных перед обработкой действий