Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Події, Alpine і навігація

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

12 питань

Компоненти на сторінці незалежні й не знають одне про одного. Події - спосіб повідомити інших, що щось сталося, не створюючи прямої залежності.

Відправити подію з PHP:

public function save(): void
{
    $post = Post::create($this->form->all());

    $this->dispatch('post-created', postId: $post->id);
}

Слухати - атрибут #[On] над методом:

use Livewire\Attributes\On;

new class extends Component {
    #[On('post-created')]
    public function refreshList(int $postId): void
    {
        unset($this->posts);   // скинути кеш обчислюваної властивості
    }
};

Параметри події передаються в метод за назвами.

Кому дістанеться подія:

  • за замовчуванням - усім компонентам на сторінці, що слухають цю назву;
  • конкретному компоненту - $this->dispatch('post-created')->to(Dashboard::class);
  • лише самому собі - $this->dispatch('post-created')->to(self: true).

Події з шаблону - без власного методу-посередника:

<button wire:click="$dispatch('open-modal', { id: {{ $post->id }} })">Редагувати</button>
<button wire:click="$dispatchTo('posts', 'refresh')">Оновити список</button>

Подія від дочірнього компонента до батька можна слухати прямо на тегу:

<livewire:edit-post :$post @saved="close" />

Як це працює всередині: подія з PHP повертається в браузер разом з відповіддю, а вже там Livewire надсилає запити компонентам-слухачам. Тобто подія - це щонайменше два запити, а не виклик методу на сервері.

Можливо, подія не потрібна:

  • дитина хоче викликати метод батька - wire:click="$parent.remove({{ $id }})";
  • дитина має оновлюватися зі зміною даних батька - #[Reactive];
  • обидві частини в одному компоненті - острівці.

Назви подій - у стилі kebab-case з «простором імен» (cart-updated, post-created), щоб не зіткнутися з подіями сторонніх бібліотек.

Докладніше в документації: Події

wire:navigate на посиланні перетворює звичайний перехід на SPA-навігацію: сторінка завантажується через fetch, а браузер не перезавантажується.

<nav>
    <a href="/" wire:navigate>Головна</a>
    <a href="/jobs" wire:navigate>Вакансії</a>
    <a href="/posts" wire:navigate.hover>Блог</a>
</nav>

Що відбувається при кліку:

  1. Livewire перехоплює клік і запитує нову сторінку звичайним GET-запитом;
  2. замінює вміст <body> і оновлює <head> (заголовок, мета-теги);
  3. змінює адресу через History API - кнопки «Назад» і «Вперед» працюють;
  4. показує смугу прогресу вгорі сторінки.

Чому швидше: CSS і JavaScript не завантажуються й не виконуються заново, Alpine і Livewire не ініціалізуються з нуля. Сторінки відкриваються помітно швидше, і застосунок відчувається як SPA, хоча сервер віддає звичайний HTML.

Попереднє завантаження:

  • за замовчуванням Livewire починає запит, щойно користувач натиснув кнопку миші, - до того, як він її відпустив;
  • .hover - починати завантаження після 60 мс наведення чи фокусу на посиланні. Ще швидше для користувача, але сервер отримує запити на сторінки, які можуть і не знадобитися.

Корисні можливості:

  • $this->redirect('/posts', navigate: true) - SPA-перехід після дії компонента;
  • @persist('player') - елемент (аудіоплеєр, бічна панель) переживає перехід без перестворення;
  • активне посилання - атрибут data-current на посиланні поточної сторінки чи директива wire:current="font-bold";
  • анімація переходу - wire:transition.navigate через View Transitions API браузера.

Що варто знати:

  • сервер віддає повну сторінку, як звичайно, - маршрути, middleware, SEO не змінюються. Без JavaScript посилання працюють як звичайні;
  • JavaScript сторінок треба адаптувати: DOMContentLoaded спрацьовує лише на першому завантаженні - замість нього livewire:navigated;
  • гостьові сторінки з кешуванням на CDN працюють з wire:navigate, але залежать від того, що віддає кеш - варто перевірити, що кешований HTML не містить персональних даних.

Докладніше в документації: Навігація

Параметри дії передаються як аргументи виклику:

<button wire:click="addToCart({{ $product->id }}, 2)">У кошик</button>
<button wire:click="setStatus('archived')">В архів</button>
public function addToCart(int $productId, int $qty = 1): void
{
    $product = Product::query()->published()->findOrFail($productId);
    $this->cart->add($product, min($qty, 10));
}

Моделі й енуми як параметри. Якщо параметр типізований моделлю, Livewire знайде її за переданим ключем (як прив'язка моделей у маршрутах):

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

Параметри - це введення користувача. У консолі браузера можна викликати $wire.addToCart(999, 100000). Тип перевіряє PHP, а межі, існування запису й права - лише ваш код.

Не тільки клік. Після wire: можна вказати будь-яку подію браузера:

<input wire:keydown.enter="search">
<input wire:keydown.escape="clear">
<div wire:mouseenter="loadPreview({{ $id }})">
<trix-editor wire:trix-change="setContent($event.target.value)">

Модифікатори (працюють через x-on Alpine):

Модифікатор Що робить
.prevent preventDefault()
.stop stopPropagation()
.once спрацювати один раз
.self лише якщо подія виникла на самому елементі
.window, .document слухати на window / document
.outside клік поза елементом
.debounce.300ms, .throttle.500ms обмеження частоти
.shift.enter, .ctrl, .escape клавіші й комбінації

Модифікатори Livewire 4:

  • .async - виконати дію паралельно, не чекаючи черги;
  • .renderless - без повторного рендеру;
  • .preserve-scroll - зберегти позицію прокрутки після оновлення.

$event - об'єкт події браузера в виразі: wire:change="setColor($event.target.value)".

Магічні дії без методу: $refresh (перерендерити), $set('tab', 'settings') (встановити властивість), $toggle('open'), $dispatch(...), $parent.method().

Стан завантаження: елемент, що ініціював запит, автоматично отримує атрибут data-loading - кнопку легко стилізувати через Tailwind (data-loading:opacity-50).

Докладніше в документації: wire:click

Обидва способи показують чи ховають частину інтерфейсу, але працюють на різних рівнях.

@if - на сервері. Blade вирішує, чи потрапить розмітка в HTML взагалі:

@if ($showModal)
    <div class="modal">...</div>
@endif

Щоб показати вікно, потрібен запит на сервер: змінити $showModal, перерендерити компонент, отримати новий HTML.

wire:show - у браузері. Елемент завжди є в DOM, а директива перемикає display: none за виразом:

<button type="button" x-on:click="$wire.showModal = true">Новий пост</button>

<div wire:show="showModal" class="modal">
    <textarea wire:model="content"></textarea>
    <button wire:click="save">Опублікувати</button>
</div>

Відкриття вікна - без запиту: $wire.showModal = true змінює значення в браузері, і wire:show реагує одразу. На сервер значення піде з наступною дією. Після save() на сервері можна виставити $this->showModal = false - і вікно закриється.

Коли wire:show:

  • миттєве перемикання (модальні вікна, вкладки, розгортання деталей), де затримка запиту помітна;
  • вміст дешевий і може бути в HTML завжди;
  • потрібні анімації появи - wire:show поєднується з x-transition Alpine, бо лише перемикає display.

Коли @if:

  • вміст не повинен потрапити в HTML - дані, доступні лише певній ролі. Прихований wire:show елемент усе одно є в коді сторінки й видний у DevTools;
  • вміст дорогий для рендеру (запити до бази в шаблоні) і потрібен рідко;
  • великі блоки, які збільшують розмір відповіді на кожному рендері.

Пов'язані директиви:

  • wire:text="expression" - вставити текст виразу без запиту (лічильник символів, попередній перегляд);
  • wire:cloak - сховати елемент до ініціалізації Livewire, щоб не «блимали» елементи, які мали бути приховані;
  • Alpine x-show - те саме для стану, що живе лише в Alpine, без Livewire.

Пастка безпеки: wire:show - це про вигляд, а не про доступ. Кнопка «Видалити», прихована через wire:show, усе одно викликає метод, який можна викликати й без кнопки, - перевірка прав потрібна в методі.

Докладніше в документації: wire:show

Події Livewire - це події браузера, тож їх бачать і Alpine, і будь-який JavaScript на сторінці.

З PHP у JavaScript. Подія, відправлена з компонента, доходить у браузер:

$this->dispatch('notify', message: 'Збережено', type: 'success');
{{-- Alpine-компонент сповіщень у макеті --}}
<div x-data="{ items: [] }"
     x-on:notify.window="items.push($event.detail)">
    ...
</div>
// звичайний JavaScript
Livewire.on('notify', ({ message, type }) => showToast(message, type));

З JavaScript у Livewire. Компонент з #[On('cart-updated')] отримає подію, відправлену будь-звідки:

<button x-on:click="$dispatch('cart-updated', { count: 3 })">...</button>
Livewire.dispatch('cart-updated', { count: 3 });
Livewire.dispatchTo('cart-badge', 'cart-updated', { count: 3 });

У скрипті компонента (Livewire 4: тег <script> у шаблоні, this - це $wire):

<script>
    this.$on('post-created', () => { /* ... */ });
    this.$dispatchSelf('refresh');
</script>

Події браузера на корені компонента - wire:назва-події:

<div wire:custom-event="handle">
    <button x-on:click="$dispatch('custom-event')">...</button>   {{-- спливе до кореня --}}
</div>
<div wire:custom-event.window="handle">   {{-- подія з будь-якого місця сторінки --}}

Безпека - головне. Слухач #[On] - це метод, який будь-хто може викликати з консолі браузера з довільними параметрами: Livewire.dispatch('order-paid', { orderId: 5 }). Тож слухачі мають перевіряти права й дані так само, як дії. Подія order-paid, що сама по собі змінює статус замовлення, - діра.

Прибирання слухачів. Livewire.on() повертає функцію для відписки. В Alpine-компонентах, що живуть із wire:navigate, слухачі треба знімати в destroy(), інакше після кожного переходу вони накопичуватимуться:

Alpine.data('cartBadge', () => ({
  off: null,
  init() { this.off = Livewire.on('cart-updated', ({ count }) => (this.count = count)); },
  destroy() { this.off?.(); },
}));

Трансляція з сервера (Echo) - слухачі виду #[On('echo:orders,OrderShipped')] отримують події WebSocket від Laravel Reverb чи Pusher без власного JavaScript.

Докладніше в документації: Події: JavaScript і Alpine

При SPA-переході через wire:navigate вміст <body> замінюється новим. Але деякі елементи не повинні перестворюватися: аудіо- чи відеоплеєр, що грає, чат-віджет, бічна панель з прокручуванням.

@persist позначає елемент, який Livewire переносить на нову сторінку замість заміни:

{{-- resources/views/layouts/app.blade.php --}}
<body>
    <main>{{ $slot }}</main>

    @persist('player')
        <audio src="{{ $episode->file }}" controls></audio>
    @endpersist

    @livewireScripts
</body>

Якщо на новій сторінці є @persist з тією самою назвою, Livewire бере існуючий DOM-елемент зі старої сторінки й ставить на місце нового. Відтворення не переривається, зберігаються стан Alpine, обробники подій і значення полів.

Умови:

  • працює лише з wire:navigate - при звичайному переході чи оновленні сторінки все завантажується заново;
  • елемент з тією самою назвою має бути на обох сторінках. Найпростіше - розмістити його в спільному макеті;
  • розміщувати поза Livewire-компонентами сторінки, зазвичай прямо в макеті. Вміст, який має оновлюватися з сервера, не варто «заморожувати».

Прокрутка всередині збереженого елемента (довга бічна навігація) - wire:navigate:scroll:

@persist('sidebar')
    <aside class="overflow-y-auto" wire:navigate:scroll>...</aside>
@endpersist

У Livewire 3 для цього був wire:scroll - при оновленні до v4 його треба перейменувати.

Підводні камені:

  • збережений елемент не оновлюється. Якщо в ньому показано «Активний пункт меню» чи лічильник, після переходу вони лишаться старими. Активне посилання в збереженій навігації позначається через data-current / wire:current - Livewire оновлює їх при навігації;
  • назва має бути унікальною й стабільною - різні елементи з однією назвою на різних сторінках підмінять один одного;
  • персональні дані в збереженому елементі (наприклад, після виходу з облікового запису через wire:navigate) переживуть перехід - вихід краще робити повним перезавантаженням сторінки.

Докладніше в документації: @persist

Сторонні бібліотеки (редактори тексту, вибір дати, карти, графіки) самі змінюють DOM: додають елементи, класи, атрибути. Після наступного запиту Livewire морфить DOM за новим HTML із сервера - і затирає все, що побудувала бібліотека.

wire:ignore каже Livewire не чіпати вміст елемента при оновленнях:

<div wire:ignore>
    <div x-data x-init="initEditor($el)"></div>
</div>

wire:ignore.self - ігнорувати зміни лише атрибутів самого елемента, а вміст оновлювати як звичайно.

Типовий шаблон інтеграції через Alpine:

<div wire:ignore
     x-data="{ value: $wire.entangle('content') }"
     x-init="
        const editor = new Editor($refs.editor, { content: value });
        editor.on('change', () => $wire.content = editor.getHTML());
     ">
    <div x-ref="editor"></div>
</div>

Краще без entangle - напряму через $wire:

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

Що варто врахувати:

  • напрям «сервер → бібліотека»: якщо сервер змінив значення (скинули форму), бібліотека під wire:ignore про це не знає. Потрібна подія ($this->dispatch('editor-reset')) чи $wire.$watch('date', ...) в Alpine;
  • частота запитів: $wire.content = ... лише змінює значення в браузері, на сервер воно піде з наступною дією. $wire.set('content', value) надсилає запит одразу - для редактора тексту це запит на кожне натискання клавіші;
  • знищення: бібліотеки, що підписуються на window/document, мають прибиратися в destroy() Alpine-компонента - інакше з wire:navigate слухачі накопичуються;
  • wire:key, якщо таких полів кілька в циклі, - щоб при зміні списку бібліотека не опинилася «прив'язаною» до чужого рядка;
  • власне поле з wire:model: обгортку можна оформити Blade-компонентом з x-modelable, тоді в шаблонах вистачить <x-date-picker wire:model="date" />.

Альтернатива - бібліотеки, створені для Livewire/Alpine (Flux, Alpine-плагіни), які вже вміють жити з морфінгом.

Докладніше в документації: wire:ignore

Livewire дає два способи виконати JavaScript, і вони вирішують різні задачі.

1. #[Js] - метод, що виконується в браузері без запиту. PHP-метод повертає рядок JavaScript; при виклику з шаблону сервер не задіюється:

use Livewire\Attributes\Js;

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

    #[Js]
    public function clearForm(): string
    {
        return <<<'JS'
            $wire.title = ''
            $wire.content = ''
        JS;
    }
};
<button type="button" wire:click="clearForm">Очистити</button>

Очищення полів відбувається миттєво. Код JavaScript потрапляє в сторінку під час рендеру, тож на клік не потрібен мережевий запит.

2. $this->js() - виконати JavaScript після дії на сервері:

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

    $this->js("document.getElementById('comment-form').reset()");
    $this->js('$wire.$refresh()');
}

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

3. Скрипти в шаблоні (Livewire 4): тег <script> у шаблоні однофайлового чи багатофайлового компонента виконується з this = $wire, без обгортки @script:

<script>
    this.$js.focusSearch = () => this.$refs.search.focus();
</script>

<input wire:ref="search">
<button type="button" wire:click="$js.focusSearch">Пошук</button>

Такі скрипти віддаються окремими кешованими файлами.

Коли що:

  • суто клієнтська дія (очистити, згорнути, сфокусуватися) - #[Js] чи $js у скрипті компонента;
  • реакція на результат серверної дії (прокрутити до нового коментаря, закрити вікно, сповіщення) - $this->js() або подія, яку слухає JavaScript;
  • переважно клієнтська логіка - Alpine, а не JavaScript, згенерований у PHP.

Пастки:

  • не вставляйте дані користувача в рядок JavaScript - $this->js("alert('{$this->name}')") з ім'ям '); stealCookies(); (' виконає чужий код. Дані передавайте через подію з параметрами чи Js::from($value) для безпечного екранування;
  • CSP: з увімкненим csp_safe складні вирази JavaScript у директивах обмежені - перевірте сумісність, якщо сайт використовує суворий Content Security Policy;
  • #[Js]-метод не має доступу до PHP-стану на момент кліку - лише до того, що є в $wire у браузері.

Докладніше в документації: Атрибут Js

Livewire 4 замінив хуки commit і request з v3 на інтерсептори трьох рівнів.

Рівень Що це API
дія виклик одного методу (save, $refresh) $wire.intercept(), Livewire.interceptAction()
повідомлення оновлення одного компонента (може містити кілька дій і змін властивостей) $wire.interceptMessage(), Livewire.interceptMessage()
запит HTTP-запит (може містити повідомлення кількох компонентів) $wire.interceptRequest(), Livewire.interceptRequest()

Кожен інтерсептор повертає функцію відписки.

Дія - найточніший рівень:

<script>
    this.intercept('delete', ({ action, onSuccess, onError }) => {
        if (! confirm('Видалити?')) {
            action.cancel();
            return;
        }
        onSuccess(() => showToast('Видалено'));
        onError(({ preventDefault }) => {
            preventDefault();          // не показувати модальне вікно помилки
            showToast('Не вдалося видалити', 'error');
        });
    });
</script>

Колбеки: onSend, onCancel, onSuccess (з поверненим значенням методу), onError (відповідь з помилкою сервера), onFailure (мережева помилка), onFinish.

Повідомлення - доступ до корисного навантаження й етапів застосування відповіді: onSend({ payload }) (знімок, оновлення, виклики), onSuccess з вкладеними onSync, onEffect, onMorphed, onRender. Корисно, щоб ініціалізувати сторонні бібліотеки після морфінгу DOM.

Запит - глобальні речі на рівні HTTP:

Livewire.interceptRequest(({ onError }) => {
  onError(({ response, preventDefault }) => {
    if (response.status === 419) {          // сесія чи CSRF-токен застаріли
      preventDefault();
      if (confirm('Сесія завершилася. Оновити сторінку?')) location.reload();
    }
  });
});

Також onRedirect (скасувати редирект), onDump (перехопити вивід dd()), onResponse, onStream.

Типові застосування:

  • глобальна обробка помилок - 419 (сесія), 403, 500 у вигляді власного повідомлення замість типового модального вікна з HTML помилки;
  • індикатори завантаження для складних випадків, де wire:loading замало;
  • аналітика й моніторинг - час відповіді, помилки, назви дій;
  • додаткові заголовки запиту на рівні застосунку.

Порядок для успішного повідомлення: onSuccess → onSync → onEffect → onMorph → onMorphed → onFinish → onRender (у наступному кадрі анімації). Код, що читає оновлений DOM, - в onMorphed, а не в onSuccess.

Що варто пам'ятати: інтерсептори - клієнтський код. Скасування дії в інтерсепторі (action.cancel()) - це UX, а не захист: дію можна викликати й без нього.

Докладніше в документації: JavaScript: інтерсептори

Звичайна дія Livewire повертає результат один раз, наприкінці. Для відповіді чат-бота, що генерується 20 секунд, користувач бачив би порожній екран усі 20 секунд. wire:stream дає змогу відправляти частини відповіді в браузер під час виконання дії.

new class extends Component {
    public string $prompt = '';
    public string $question = '';
    public string $answer = '';

    public function submitPrompt(): void
    {
        $this->question = $this->prompt;
        $this->prompt = '';
        $this->answer = '';

        $this->js('$wire.ask()');   // друга дія - вже після того, як поле очистилося
    }

    public function ask(): void
    {
        // streamAnswer() - генератор над будь-яким клієнтом LLM зі стримінгом
        foreach ($this->streamAnswer($this->question) as $chunk) {
            $this->answer .= $chunk;
            $this->stream(to: 'answer', content: $chunk);
        }
    }
};
<p wire:stream="answer">{{ $answer }}</p>

<form wire:submit="submitPrompt">
    <input wire:model="prompt" placeholder="Ваше питання">
</form>

Як це працює: відповідь на запит ask() стає потоковою: кожен $this->stream() одразу відправляє шматок у браузер, і Livewire дописує його в елемент з wire:stream="answer". Коли метод завершується, приходить звичайна фінальна відповідь з повним станом компонента.

Параметри stream():

  • to: - назва цілі (значення wire:stream в шаблоні);
  • content: - текст;
  • replace: true - замінити вміст замість дописування (для індикатора прогресу «Оброблено 40%»).

Чому дві дії (submitPrompt + ask): перша швидко повертає відповідь - поле введення очищується, питання з'являється на екрані. Друга вже стрімить відповідь.

Що враховувати в продакшені:

  • процес PHP зайнятий весь час стримінгу. Двадцять секунд генерації - двадцять секунд зайнятого воркера PHP-FPM. При багатьох одночасних користувачах це вичерпує пул; доречні Octane/FrankenPHP чи винесення генерації в чергу з трансляцією результату через Reverb;
  • буферизація по дорозі: проксі (Nginx proxy_buffering, CDN), стиснення відповідей можуть накопичувати шматки й віддавати все наприкінці. Потрібно вимкнути буферизацію для цих запитів;
  • тайм-аути: max_execution_time, тайм-аути проксі й балансувальника мають покривати найдовшу генерацію;
  • зберігати повний результат у властивості ($this->answer .= $chunk) - інакше після фінальної відповіді та наступного рендеру текст зникне;
  • XSS: потоковий вміст вставляється як текст, але якщо ви рендерите відповідь як Markdown/HTML, санітизуйте її на сервері.

Альтернативи: response()->eventStream() з SSE у звичайному маршруті Laravel, якщо компонентна модель не потрібна.

Докладніше в документації: wire:stream

З wire:navigate браузер фактично не залишає першу сторінку: замінюється вміст <body>, а JavaScript-оточення живе далі. Код, написаний для звичайних переходів, починає поводитися інакше.

1. DOMContentLoaded спрацьовує лише один раз. Ініціалізація на ньому не виконається на наступних сторінках:

// було
document.addEventListener('DOMContentLoaded', initTooltips);

// стало - спрацьовує і при першому завантаженні, і після кожного переходу
document.addEventListener('livewire:navigated', initTooltips);

2. Скрипти в <head> виконуються один раз. Нові скрипти, яких не було на попередній сторінці, - виконуються при переході й блокують перехід, поки не завантажаться.

3. Скрипти в <body> виконуються на кожній сторінці заново. Ініціалізація, що має бути одноразовою (аналітика, глобальні обробники), отримує атрибут data-navigate-once. Інакше - подвійні події аналітики й подвійні обробники.

4. Накопичення слухачів. Обробник на window чи document, доданий на кожній сторінці, після десятка переходів спрацьовує десять разів. Знімати в destroy() Alpine чи використовувати { once: true } для одноразових.

5. Застарілі ассети після деплою. Користувач не перезавантажує сторінку годинами і працює зі старим JavaScript. Атрибут data-navigate-track на тегах ассетів змушує Livewire зробити повне перезавантаження, якщо змінився рядок запиту в URL ассета. Директива @vite додає його автоматично.

6. Мерехтіння теми. Тема (dark) застосовується скриптом після завантаження - при навігації між сторінками з різними класами <html> інтерфейс «блимає». Застосовуйте її в livewire:navigating через e.detail.onSwap() - до того, як новий HTML з'явиться на екрані.

7. Аналітика. Перегляд сторінки треба відправляти на livewire:navigated, інакше аналітика бачить лише першу сторінку сесії.

8. Стан сторонніх бібліотек. Каруселі, карти, редактори з першої сторінки лишаються в пам'яті, якщо їх не знищити, - витоки пам'яті за годину роботи.

Події життєвого циклу навігації:

  • livewire:navigate - перехід почався (можна скасувати preventDefault());
  • livewire:navigating - новий HTML отримано, перед заміною (місце для onSwap);
  • livewire:navigated - усе завершено (також на першому завантаженні).

Коли варто відмовитися від wire:navigate для посилання: вихід з облікового запису, перемикання мови, переходи між різними макетами чи застосунками - там надійніше звичайне перезавантаження.

Докладніше в документації: Навігація: виконання скриптів

Livewire 4 має вбудоване сортування перетягуванням - без сторонніх бібліотек.

<ul wire:sort="reorder">
    @foreach ($this->tasks as $task)
        <li wire:key="{{ $task->id }}" wire:sort:item="{{ $task->id }}">
            <span wire:sort:handle>⋮⋮</span>
            {{ $task->title }}
        </li>
    @endforeach
</ul>

Коли користувач відпускає елемент, Livewire викликає метод з ідентифікатором елемента (wire:sort:item) і новою позицією (з нуля). wire:sort:handle обмежує перетягування «ручкою».

Між кількома списками (канбан-дошка) - однакова група і ідентифікатор контейнера, що стає третім параметром:

<ul wire:sort="moveCard" wire:sort:group="cards" wire:sort:group-id="{{ $column->id }}">

Збереження порядку - ваша відповідальність, і тут легко помилитися:

public function reorder(int $taskId, int $position): void
{
    $this->authorize('update', $this->project);

    DB::transaction(function () use ($taskId, $position) {
        $ids = $this->project->tasks()->orderBy('position')->lockForUpdate()->pluck('id');

        abort_unless($ids->contains($taskId), 404);   // лише задачі цього проєкту

        $position = max(0, min($position, $ids->count() - 1));

        $ordered = $ids->reject(fn ($id) => $id === $taskId)->values();
        $ordered->splice($position, 0, [$taskId]);

        foreach ($ordered as $index => $id) {
            Task::whereKey($id)->update(['position' => $index]);
        }
    });

    unset($this->tasks);
}

Що тут важливо:

  • ідентифікатор і позиція - від клієнта. Без перевірки, що задача належить проєкту користувача, можна пересунути чужу задачу. Пошук через зв'язок ($this->project->tasks()) чи findOrFail в області видимості власника обов'язковий;
  • межі позиції - від'ємна чи завелика позиція не повинна ламати нумерацію;
  • перенумерація в транзакції з блокуванням - два користувачі, що одночасно сортують один список, інакше отримають дублікати позицій;
  • для довгих списків оновлювати лише зсунутий діапазон (increment/decrement для позицій між старою й новою), а не всі рядки;
  • wire:key на елементах обов'язковий - після відповіді сервера морфінг має зіставити елементи за ідентифікатором, а не за позицією;
  • для переміщення між колонками перевіряти й колонку призначення (третій параметр) - вона теж може бути чужою.

Альтернатива перенумерації - дробові чи «розріджені» позиції (крок 1000, вставка посередині між сусідами): одне оновлення замість багатьох, з періодичною перенумерацією.

Докладніше в документації: wire:sort