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

Livewire і Filament: питання на співбесіді рівня Middle

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

41 питань

Livewire сильний там, де інтерфейс - це форми, таблиці, фільтри й адмінки поверх серверних даних: логіка й валідація в PHP, без окремого API і дублювання правил на фронтенді. Але кожна його взаємодія - це запит на сервер і рендер компонента, і з цього випливають межі.

Коли Livewire - поганий вибір:

  • миттєва реакція на кожен рух: перетягування з анімацією, малювання, редактори з частими змінами, ігри. Затримка мережі (навіть 50-100 мс) помітна;
  • робота офлайн чи на нестабільному з'єднанні - без сервера інтерфейс не працює;
  • складний клієнтський стан, що майже не стосується сервера: багатокрокові конструктори, полотна, планувальники з десятками елементів на екрані;
  • кілька клієнтів одного API: мобільний застосунок і вебверсія - API все одно потрібен, і Livewire додає другий шлях до тих самих даних;
  • висока кількість одночасних користувачів з частими діями: кожна дія навантажує PHP-воркери, тоді як SPA ходить на сервер лише за даними.

Що обрати замість:

Ситуація Інструмент
дрібна інтерактивність без сервера: меню, вкладки, модальні вікна Alpine.js - разом з Livewire або без нього
багатий інтерфейс на React чи Vue, але маршрутизація й дані з Laravel без окремого API Inertia
кілька клієнтів, офлайн, окрема фронтенд-команда SPA чи мобільний застосунок + API (Sanctum)
сайт зі статичним вмістом звичайний Blade, кеш на краю мережі

Поєднання частіше, ніж вибір:

  • Livewire + Alpine: клієнтські дрібниці на Alpine, дані й збереження на Livewire. $wire у Alpine дає доступ до властивостей і методів компонента;
  • острівці й ізоляція: важку частину сторінки винести в окремий компонент чи острівець, щоб її оновлення не перерендерювали все;
  • окремий віджет на JavaScript (графік, редактор) усередині Livewire-сторінки з wire:ignore.

Як зважувати: склад команди (PHP-розробники чи фронтенд), вимоги до швидкості реакції, кількість клієнтів API. Переписування з Livewire на SPA посеред проєкту дороге - тому питання «чи буде мобільний застосунок» варто поставити на початку.

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

wire:model приймає не лише ім'я властивості, а й шлях усередині неї через крапку.

Масиви й вкладені дані:

public array $address = ['city' => '', 'street' => ''];
public array $items = [];
public array $roles = [];
<input wire:model="address.city">
<input wire:model="address.street">

@foreach ($items as $index => $item)
    <div wire:key="item-{{ $item['id'] }}">
        <input wire:model="items.{{ $index }}.qty">
    </div>
@endforeach

{{-- кілька чекбоксів у масив --}}
<input type="checkbox" value="editor" wire:model="roles">
<input type="checkbox" value="author" wire:model="roles">

Хук updated отримує повний шлях: updatedItems($value, $key) з $key = '2.qty' - видно, який рядок змінився.

Об'єкти форм - те саме, але властивості типізовані й валідація поруч:

<input wire:model="form.title">

Чому не модель напряму. У Livewire 3 і 4 цей код кидає виняток:

public Post $post;
<input wire:model="post.title">   {{-- Can't set model properties directly --}}

Причини:

  • безпека: властивості компонента - це дані, які надсилає браузер. Зв'язування з моделлю дозволило б змінювати будь-який атрибут моделі запитом, включно з тими, що не виводяться у формі (is_admin, user_id), - аналог масового присвоєння без $fillable;
  • стан: модель серіалізується лише як клас і ключ, а при кожному запиті завантажується з бази заново - незбережені зміни атрибутів між запитами губилися б.

Як правильно: окремі властивості чи об'єкт форми, а при збереженні - явне заповнення провалідованими даними:

public function save(): void
{
    $this->post->update($this->form->validate());
}

Стару поведінку можна ввімкнути (legacy_model_binding у конфігурації разом з правилами валідації для кожного поля), але для нового коду це не рекомендований шлях.

Пастки з масивами:

  • без wire:key у циклі після видалення рядка введені значення «переїжджають» в сусідні поля;
  • індекси масиву після видалення: unset($this->items[$i]) лишає дірку в ключах; array_values() після видалення - і wire:key за стабільним ідентифікатором, а не індексом;
  • великий масив у властивості - весь масив серіалізується в кожен запит. Для сотень рядків краще окремі дочірні компоненти чи редагування по одному.

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

Filament - фреймворк Server-Driven UI для Laravel: інтерфейси (адмінки, панелі) описуються на PHP через структуровані об'єкти, а не верстку.

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('title')->required(),
        Select::make('status')->options(Status::class),
    ]);
}
  • Побудований на Livewire, Alpine.js і Tailwind CSS.
  • Будівельні блоки: Resources, Forms, Tables, Actions, Infolists, Widgets.
  • Компоненти ініціалізуються статичними make()-методами; динамічні значення задаються замиканнями з утилітами Get/Set.

Усе публічне в компоненті доступне з браузера, а не лише те, що є в шаблоні.

Публічні методи можна викликати з консолі DevTools з будь-якими аргументами, навіть якщо в шаблоні на них немає wire:click:

public function delete(int $id): void
{
    $post = Post::findOrFail($id);
    $this->authorize('delete', $post);   // без цього - видалить будь-що
    $post->delete();
}

Кнопка «Видалити», яку бачать лише адміністратори, нічого не захищає: метод однаково можна викликати.

Публічні властивості зберігаються на клієнті між запитами й можуть бути змінені:

public int $postId;   // користувач може підставити чужий ID

Що робити:

  • Авторизувати кожну дію на сервері - так само, як у контролері: політики, authorize(), перевірка власника.
  • #[Locked] на властивостях, які не мають змінюватися з клієнта, - Livewire кине виняток при спробі підміни.
  • Модель замість ID: якщо у властивості лежить Eloquent-модель (public Post $post), Livewire сам стежить, щоб її ID не підмінили.
  • Внутрішні методи - protected чи private: допоміжний public function deleteQuietly() - теж ендпоінт.
  • Не класти в публічні властивості секрети: вони потрапляють у HTML-знімок компонента.

Правило: Livewire-метод - це маршрут, а його параметри й публічні властивості - запит. Усе, що ви перевіряли б у контролері, перевіряйте й тут.

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

Компонент Livewire не живе на сервері між запитами - на кожен запит його створюють заново зі знімка. Хуки дають змогу виконати код у потрібний момент цього циклу.

Хук Коли
mount() один раз, при створенні компонента (перший рендер)
boot() на кожен запит: і перший, і наступні
hydrate() на кожен наступний запит, після відновлення зі знімка (не на першому)
updating($property, $value) перед зміною властивості з браузера
updated($property) після зміни властивості
rendering() / rendered() до й після рендеру шаблону
dehydrate() наприкінці кожного запиту, перед серіалізацією
exception($e, $stopPropagation) при винятку в компоненті

mount() замість конструктора - приймає параметри (атрибути тегу, параметри маршруту) і задає початковий стан:

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

boot() - для того, що не зберігається між запитами: захищені (protected) властивості, сервіси. Вони не потрапляють у знімок, тож без boot() на наступних запитах будуть порожніми.

updating / updated - реакція на зміну конкретної властивості, з назвою в методі:

public function updatedSearch(): void
{
    $this->resetPage();   // новий пошук - на першу сторінку пагінації
}

public function updatedPreferences($value, $key): void
{
    // для масивів: $key - змінений ключ ('theme'), $value - нове значення
}

updating може кинути виняток і не дати змінити властивість - але для захисту від підробки краще #[Locked].

Пастки:

  • updated спрацьовує лише на зміни з браузера (wire:model, $set), а не на присвоєння в PHP-коді: $this->search = 'x' у методі хук не викличе;
  • важкі запити в boot() чи hydrate() виконуються на кожен запит, навіть коли дані не потрібні. Для даних, що потрібні лише в шаблоні, кращі обчислювані властивості (#[Computed]);
  • хуки працюють і в трейтах (bootWithSorting, updatedWithSorting) та в об'єктах форм.

Докладніше в документації: Хуки життєвого циклу

Між запитами стан компонента серіалізується в JSON і назад, тож публічна властивість може мати лише тип, який Livewire уміє перетворити.

Підтримується з коробки:

  • примітиви: string, int, float, bool, array, null;
  • BackedEnum, Collection, Eloquent Model і Eloquent\Collection, DateTime, Carbon, Stringable;
  • власні типи - через Wireable чи синтезатори.

Замикання, ресурси, незбережувані сервіси в публічну властивість покласти не можна. Для них - protected властивості, ініціалізовані в boot().

Як серіалізуються моделі: у знімок потрапляє не вміст моделі, а клас і ключ (для колекції - клас і список ключів):

"post": [null, { "class": "App\\Models\\Post", "key": 1, "s": "mdl" }]

На кожному наступному запиті Livewire завантажує модель з бази заново. На PHP 8.4+ Livewire 4 відновлює її як лінивий проксі: запит виконується лише при першому зверненні до моделі в цьому запиті.

Наслідки:

  • запит до бази на кожну дію, що торкається моделі, а для колекції - whereIn за всіма ключами. Колекція на тисячу моделей у властивості - тисяча рядків з бази на кожен клік;
  • завантажені зв'язки не відновлюються. Post::with('author') при першому завантаженні не діє на наступних запитах - шаблон, що виводить $post->author для кожного елемента, отримує N+1;
  • обмеження запиту губляться: ->select(['id', 'title']) чи умови, накладені при першому завантаженні, на наступних запитах не застосуються - модель завантажиться повністю;
  • назва класу видна в браузері. Приховати її допомагає Relation::morphMap() - тоді в знімку буде псевдонім;
  • зміни моделі, не збережені в базу, губляться між запитами - відновлюється те, що в базі.

Добра новина: ідентифікатор моделі в публічній властивості автоматично захищений від підміни - як з #[Locked].

Що робити замість великих колекцій у властивостях:

#[Computed]
public function posts()
{
    return Post::query()->where('author_id', $this->authorId)->latest()->paginate(20);
}
@foreach ($this->posts as $post) ... @endforeach

Обчислювана властивість виконується лише тоді, коли її читає шаблон, і не потрапляє в знімок. Публічні властивості лишають для стану інтерфейсу: фільтрів, введених значень, ідентифікаторів.

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

У Livewire кожен компонент незалежний: має власний знімок стану і оновлюється окремим запитом. Параметри, передані дочірньому компоненту, потрапляють у його mount() один раз - при створенні.

{{-- батьківський --}}
<livewire:todo-count :$todos />

Якщо батьківський компонент додав завдання, todo-count про це не дізнається: при оновленні батька дочірні компоненти не рендеряться заново - замість них Livewire вставляє «заглушки» й пропускає їх під час морфінгу DOM.

Це зроблено свідомо: мінімум даних у кожному запиті. Оновлення батька не тягне за собою стан і рендер усіх нащадків.

Способи синхронізувати:

1. #[Reactive] - параметр оновлюється, коли змінюється в батьківському компоненті:

new class extends Component {
    #[Reactive]
    public $todos;
};

Ціна - при кожному оновленні батька, де змінився параметр, дочірній компонент теж оновлюється. Змінювати реактивний параметр у самому дочірньому компоненті не можна.

2. Новий wire:key - компонент створюється заново:

<livewire:todo-count :$todos :wire:key="$todos->pluck('id')->join('-')" />

3. Події - дочірній слухає #[On('todo-added')] і перечитує дані сам.

4. Острівці (@island) замість дочірнього компонента. Якщо компонент виділяли лише заради ізольованого оновлення частини екрана, острівець робить це в межах одного компонента - без параметрів і подій.

Зворотний напрямок (дитина → батько):

  • $parent.remove(id) у шаблоні дитини - прямий виклик методу батька;
  • подія, яку слухає батько;
  • #[Modelable] - щоб на дочірньому компоненті працював wire:model батька.

Обов'язкове правило для циклів: дочірні компоненти в @foreach отримують унікальний wire:key (:wire:key="$post->id"), інакше при зміні списку стан компонентів «переплутається».

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

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

Публічну властивість можна змінити з браузера: через wire:model, $wire.postId = 5 в консолі чи підробленим запитом. Для властивостей, на які спирається безпека, це небезпечно.

new class extends Component {
    public $postId;   // користувач може підмінити на чужий id

    public function delete(): void
    {
        Post::find($this->postId)->delete();   // видалить чужий пост
    }
};

#[Locked] забороняє зміну властивості з клієнта. Спроба - виняток, дія не виконується:

use Livewire\Attributes\Locked;

new class extends Component {
    #[Locked]
    public int $postId;

    public function mount(int $id): void
    {
        $this->postId = $id;
    }
};

Що #[Locked] не робить:

  • не забороняє змінювати властивість у PHP-коді компонента - якщо ви самі присвоїте їй значення з введення користувача, захисту немає;
  • не замінює авторизацію. Заблокований postId гарантує, що id не підмінили після mount(). Але чи має користувач право на цей пост узагалі, треба перевірити - у mount() і в кожній дії, що змінює дані ($this->authorize('delete', $post));
  • не приховує значення - воно все одно видно в знімку в HTML.

Коли захист є без атрибута: публічна властивість з моделлю Eloquent (public Post $post) автоматично захищена від підміни ідентифікатора. Тому передавати в компонент модель замість голого id часто і зручніше, і безпечніше.

Кандидати на #[Locked]:

  • ідентифікатори записів, з якими працює компонент;
  • ролі, прапорці доступу, ціни й знижки, обчислені на сервері;
  • будь-яке значення, що визначає, що дозволено зробити, а не що користувач вводить.

Альтернатива - не тримати таке значення в публічній властивості взагалі: protected властивість, відновлена в boot(), чи обчислювана властивість.

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

Об'єкт форми - окремий клас, куди виносять поля, правила валідації й логіку збереження. Компонент стає коротшим, а форму можна використати в кількох компонентах.

php artisan livewire:form PostForm   # app/Livewire/Forms/PostForm.php
namespace App\Livewire\Forms;

use App\Models\Post;
use Illuminate\Validation\Rule;
use Livewire\Form;

class PostForm extends Form
{
    public ?Post $post = null;

    public string $title = '';
    public string $content = '';

    protected function rules(): array
    {
        return [
            'title' => ['required', 'max:255', Rule::unique('posts')->ignore($this->post)],
            'content' => ['required', 'min:10'],
        ];
    }

    public function setPost(Post $post): void
    {
        $this->post = $post;
        $this->fill($post->only(['title', 'content']));
    }

    public function save(): Post
    {
        $this->validate();

        $post = $this->post ?? new Post(['author_id' => auth()->id()]);
        $post->fill($this->only(['title', 'content']))->save();

        return $post;
    }
}

Компоненти створення й редагування:

// створення
public PostForm $form;

public function save(): void
{
    $post = $this->form->save();
    $this->redirectRoute('posts.show', $post);
}

// редагування
public function mount(Post $post): void
{
    $this->authorize('update', $post);
    $this->form->setPost($post);
}

У шаблоні поля адресуються через назву властивості з формою:

<input wire:model="form.title">
@error('form.title') <p>{{ $message }}</p> @enderror

Корисні методи форми: $this->form->all(), only([...]), reset() (повертає значення за замовчуванням), pull() (отримати значення й одразу скинути), validate().

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

  • ключі помилок мають префікс назви властивості: form.title, а не title - і в @error, і в тестах (assertHasErrors('form.title'));
  • хуки updated... працюють і всередині об'єкта форми;
  • авторизація лишається в компоненті чи в методі форми - об'єкт форми не робить дії безпечними сам по собі;
  • reset() повертає типізовані властивості без значення за замовчуванням у неініціалізований стан - полям варто задавати значення за замовчуванням.

Аналог у звичайному Laravel - Form Request, але об'єкт форми ще й тримає стан між запитами компонента.

Докладніше в документації: Форми: об'єкти форм

Валідація в реальному часі - перевірка поля під час заповнення форми, а не лише при відправці.

Що для цього потрібно:

  1. правила на властивості через #[Validate] - Livewire перевіряє властивість при кожному оновленні з браузера;
  2. оновлення має доходити до сервера - wire:model.live чи wire:model.live.blur.
#[Validate('required|email|unique:users,email')]
public string $email = '';
<input type="email" wire:model.live.blur="email">
@error('email') <p>{{ $message }}</p> @enderror

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

Чому не просто .live: перевірка на кожне натискання клавіші показує «Некоректна адреса», поки людина ще друкує, і створює запит на кожну паузу. Перевірка при виході з поля (.blur) - зручніша і дешевша. Виняток - поля, де миттєвий відгук корисний (сила пароля, доступність імені користувача).

Чому rules() не спрацьовує при введенні. Метод rules() застосовується лише при виклику $this->validate(). При оновленні властивості Livewire перевіряє тільки ті, що мають атрибут #[Validate]. Щоб і правила з rules() (наприклад, з об'єктами Rule::unique(), яких атрибут не дозволяє) перевірялися при введенні, додайте порожній атрибут:

#[Validate]
public string $email = '';

protected function rules(): array
{
    return ['email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())]];
}

Вимкнути автоматичну перевірку для поля з атрибутом - #[Validate('required', onUpdate: false)].

Перевірити одне поле вручну: $this->validateOnly('email') у хуку updated.

Помилки в JavaScript - магічна властивість $errors:

<p wire:show="$errors.has('email')" wire:text="$errors.first('email')"></p>

Підсумкова перевірка обов'язкова: валідація в реальному часі - зручність. Перед збереженням $this->validate() все одно викликається, бо частину полів користувач міг не чіпати, а запит можна відправити й в обхід інтерфейсу.

Докладніше в документації: Валідація в реальному часі

Запит Livewire - не звичайний запит сторінки, а AJAX. HTTP-редирект у відповідь на нього браузер не виконає як перехід. Тому Livewire передає «команду перейти» в JavaScript, а той змінює сторінку.

Методи компонента:

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

    session()->flash('status', 'Пост опубліковано');

    $this->redirectRoute('posts.show', ['post' => $post]);
}
  • $this->redirect('/posts') - за адресою;
  • $this->redirectRoute('posts.show', [...]) - за назвою маршруту;
  • $this->redirectAction([PostController::class, 'index']) - на дію контролера;
  • $this->redirectIntended('/dashboard') - туди, куди користувач ішов до входу.

Повернути redirect()->route(...) з методу теж можна - Livewire розпізнає звичайну відповідь-редирект Laravel.

SPA-перехід замість повного завантаження:

$this->redirect('/posts', navigate: true);
$this->redirectRoute('posts.index', navigate: true);

Сторінка підвантажується як при wire:navigate: без перезавантаження скриптів і стилів, швидше.

Флеш-повідомлення працюють, як у звичайному Laravel, - session()->flash(), а на сторінці призначення:

@if (session('status'))
    <div class="alert">{{ session('status') }}</div>
@endif

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

  • подія в браузер, яку слухає компонент сповіщень ($this->dispatch('notify', message: 'Збережено'));
  • властивість компонента з текстом повідомлення.

Пастки:

  • код після редиректу виконується: $this->redirect() лише записує намір, а не зупиняє метод. Якщо після нього є ще логіка, вона спрацює - за потреби додайте return;
  • редирект на зовнішній домен з navigate: true не працює як SPA-перехід - для зовнішніх адрес звичайний redirect();
  • не редиректити за адресою з введення користувача без перевірки - відкритий редирект використовують для фішингу.

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

Після збереження форма, на якій користувач лишається (коментар, повідомлення в чаті, додавання рядка в список), має очиститися.

reset() повертає властивості до значень, оголошених у класі:

public string $body = '';
public ?int $rating = null;

public function addComment(): void
{
    $this->validate();
    $this->post->comments()->create($this->only(['body', 'rating']));

    $this->reset('body', 'rating');   // лише ці поля
    // $this->reset();                // усі публічні властивості
}

Обережно з reset() без аргументів: він скидає всі публічні властивості компонента, включно з тими, що встановлено в mount() (наприклад, $post), - і компонент «губить» свій контекст. Безпечніше явно перелічувати поля або тримати поля форми в об'єкті форми й скидати лише його: $this->form->reset().

pull() - отримати значення і одразу скинути:

$this->post->comments()->create($this->pull(['body', 'rating']));

resetExcept([...]) - скинути все, крім указаного.

Помилки валідації зберігаються окремо від значень:

$this->resetValidation();         // усі помилки
$this->resetValidation('body');   // помилку одного поля
$this->resetErrorBag();           // те саме, що resetValidation()

Типовий випадок - кнопка «Скасувати» в модальному вікні: скинути і поля, і помилки, щоб при наступному відкритті форма була чистою.

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

  • типізована властивість без значення за замовчуванням (public string $title;) після reset() стає неініціалізованою, і звернення до неї в шаблоні кине помилку. Задавайте значення за замовчуванням;
  • reset() не викликає хуків updated - це зміна на сервері, а не з браузера;
  • поля з wire:model без .live очищаються в інтерфейсі, бо після відповіді сервера Livewire морфить DOM з новими значеннями;
  • для полів, якими керує сторонній JavaScript (редактор тексту, вибір дати під wire:ignore), скидання властивості не очистить сам віджет - йому треба надіслати подію.

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

Острівець - частина шаблону компонента, що оновлюється ізольовано: дія всередині острівця перерендерює лише його, а не весь компонент.

<div>
    <h1>Дашборд</h1>

    @island
        <div>
            Дохід: {{ $this->revenue }}
            <button type="button" wire:click="$refresh">Оновити</button>
        </div>
    @endisland

    <livewire:recent-orders />
</div>

Клік «Оновити» перерахує й перемалює лише блок доходу. Оскільки revenue - обчислювана властивість, дорогий запит виконується тільки при рендері острівця.

Можливості:

  • ліниве завантаження частини компонента: @island(lazy: true) чи @island(defer: true) з @placeholder усередині;
  • іменовані острівці і виклик з будь-якого місця компонента:
@island(name: 'stats') ... @endisland
<button wire:click="$refresh" wire:island="stats">Оновити статистику</button>
  • дописування замість заміни - для «Завантажити ще» й нескінченної стрічки: wire:island.append="feed" чи .prepend;
  • always: true - острівець оновлюється разом з кожним рендером компонента;
  • skip: true - не рендерити, доки не викличуть явно.

Важлива поведінка за замовчуванням: коли рендериться весь компонент, острівці пропускаються. Змінили фільтр поза острівцем - острівець, що від нього залежить, сам не оновиться (потрібен always: true або явний виклик за назвою).

Чим краще за дочірній компонент:

  • немає окремого файлу, стану, параметрів і подій для синхронізації - острівець бачить властивості й методи компонента;
  • не потрібен #[Reactive] для передачі змін.

Коли все ж дочірній компонент:

  • повторне використання в кількох місцях;
  • власні хуки життєвого циклу, власна авторизація в mount();
  • складний ізольований стан.

Обмеження:

  • острівці не можна ставити в @foreach, @if та інші керуючі конструкції - вони не мають доступу до змінних циклу чи умови;
  • запити острівців ідуть паралельно і змінюють той самий стан компонента: якщо кілька запитів у польоті одночасно, «перемагає» відповідь, що прийшла останньою. Дії, що змінюють спільні властивості, варто тримати поза паралельними острівцями.

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

Після кожної дії Livewire за замовчуванням рендерить шаблон компонента заново й відправляє HTML у браузер. Для дій, що нічого не змінюють на екрані, це зайва робота: виконання Blade, обчислювані властивості й запити в шаблоні, розмір відповіді, морфінг DOM.

#[Renderless] - дія без рендеру:

use Livewire\Attributes\Renderless;

new class extends Component {
    public Post $post;

    #[Renderless]
    public function trackRead(): void
    {
        $this->post->increment('reads');
    }
};
<div wire:intersect.once="trackRead">...</div>

Інші способи:

  • $this->skipRender() усередині методу - коли рендер треба пропустити за умовою:
public function saveDraft(): void
{
    $this->post->update(['draft' => $this->draft]);

    if (! $this->showSavedAt) {
        $this->skipRender();   // показувати нічого не треба - рендер зайвий
    }
}
  • модифікатор .renderless у шаблоні - точково, без зміни класу: wire:click.renderless="trackClick", wire:model.live.renderless="draft".

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

  • аналітика й лічильники (перегляди, кліки, час на сторінці);
  • збереження чернетки чи налаштувань, які вже відображені в браузері (стан у полі чи в Alpine);
  • дії, результат яких обробляє JavaScript (повернене значення з await $wire.method()).

Пастки:

  • зміни властивостей у такій дії не з'являться на екрані - шаблон не рендерився. Якщо дія змінює те, що показується, рендер потрібен;
  • помилки валідації в дії без рендеру теж не відобразяться через @error - для таких випадків потрібен рендер чи обробка в JavaScript;
  • стан компонента все одно оновлюється - новий знімок повертається в браузер, тож наступні дії бачитимуть змінені значення.

Поєднання з #[Async] - класична пара для «запустив і забув»: дія не стає в чергу за іншими й не перемальовує компонент.

Тести: ->call('trackRead')->assertRenderSkipped().

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

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

Групування між компонентами (bundling). Якщо кілька компонентів сторінки оновлюються одночасно (подія, на яку реагують три компоненти, кілька опитувань), Livewire об'єднує їхні оновлення в один HTTP-запит. Менше з'єднань, менше навантаження, а також це основа для механізмів, що потребують узгодження компонентів (реактивні параметри, #[Modelable]).

Мінус групування: спільний запит відповідає тоді, коли закінчив найповільніший компонент. Повільний віджет з викликом зовнішнього API затримує оновлення решти сторінки.

#[Isolate] - запити компонента не групуються з іншими і йдуть паралельно:

use Livewire\Attributes\Isolate;

new #[Isolate] class extends Component {
    public function refreshShipping(): void
    {
        $this->status = Carrier::trackParcel($this->trackingNumber);   // 2-3 секунди
    }
};

Коли ізолювати:

  • дорогі операції (складні запити, зовнішні API, важкі обчислення);
  • кілька компонентів з wire:poll з різними інтервалами;
  • компоненти, що слухають ті самі події, де один повільний не повинен гальмувати інших;
  • компонент не взаємодіє з іншими через реактивні параметри.

Ціна ізоляції - більше паралельних запитів і процесів PHP. Ізолювати все підряд - отримати десятки запитів замість одного.

Пов'язані механізми:

  • ліниві компоненти ізольовані за замовчуванням - кожен вантажиться окремим паралельним запитом; lazy.bundle / #[Lazy(bundle: true)] об'єднує їх;
  • #[Async] / .async - не стосується групування між компонентами, а виводить дію з черги свого компонента: вона виконується одразу, паралельно з іншими діями;
  • Livewire 4 зробив опитування й wire:model.live неблокувальними: швидке введення в полі пошуку не чекає завершення попереднього запиту.

Діагностика: вкладка Network у DevTools - видно, скільки запитів livewire/update іде, які компоненти в кожному (у тілі запиту - масив компонентів) і скільки триває кожен.

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

Питання рівня Middle з реальних технічних співбесід - 41 питання у 8 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.

Інші рівні
Junior 36 Senior 36

Готуєтесь до співбесіди не просто так: зараз на сайті 78 відкритих вакансій рівня Middle. Переглянути вакансії