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

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

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

36 питань

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

php artisan make:filament-resource Vacancy --generate

Команда створює клас ресурсу, сторінки List/Create/Edit, а форму й таблицю виносить в окремі файли (Schemas/VacancyForm.php, Tables/VacancyTable.php). Прапорці --embed-schemas і --embed-table лишають їх усередині класу ресурсу.

Дві головні частини - форма й таблиця:

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TextInput::make('title')
            ->required()
            ->maxLength(255),
        Select::make('level')
            ->options(VacancyLevel::class)
            ->required(),
        Toggle::make('is_published'),
    ]);
}

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('title')->searchable()->sortable(),
            TextColumn::make('company.name')->label('Компанія'),
            IconColumn::make('is_published')->boolean(),
        ])
        ->filters([
            SelectFilter::make('level')->options(VacancyLevel::class),
        ]);
}

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

На чому побудовано: Livewire відповідає за реактивність без написання JS, Alpine - за дрібні взаємодії в браузері, Tailwind - за оформлення. Тому Filament - це PHP-код, а не окремий фронтенд-застосунок.

Що варто знати одразу: форма й таблиця - це звичайні PHP-масиви обʼєктів, тож видимість поля, опції списку чи доступність дії задаються замиканнями й можуть залежати від користувача та стану запису.

Кожна дія Livewire (wire:click, відправка форми, оновлення wire:model.live) - це запит на сервер. Поки він іде, користувач має бачити, що щось відбувається.

wire:loading показує елемент лише під час запиту:

<button wire:click="save">Зберегти</button>

<span wire:loading wire:target="save">Зберігаємо...</span>

Корисні модифікатори:

  • wire:target="save" - реагувати лише на конкретну дію чи властивість, а не на будь-який запит компонента.
  • wire:loading.attr="disabled" - вимкнути кнопку на час запиту, щоб не було подвійних відправок.
  • wire:loading.class="opacity-50" - додати клас.
  • wire:loading.remove - навпаки, приховати елемент під час запиту.
  • wire:loading.delay - показати індикатор лише якщо запит триває довше ~200 мс, щоб швидкі дії не «блимали».
<button wire:click="save" wire:loading.attr="disabled" wire:loading.class="opacity-50">
    Зберегти
</button>

У Livewire 4 елементи, що ініціюють запит, ще й автоматично отримують атрибут data-loading на час запиту. Тож стилізувати стан можна просто через Tailwind: class="data-loading:opacity-50", без wire:target.

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

Livewire - фреймворк для Laravel, що дає змогу будувати інтерактивні інтерфейси на PHP і Blade, без окремого фронтенду на Vue чи React. Компонент - це PHP-клас зі станом (публічні властивості) і діями (публічні методи) плюс Blade-шаблон.

new class extends Component {
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
};
<div>
    <span>{{ $count }}</span>
    <button wire:click="increment">+</button>
</div>

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

  1. JavaScript Livewire перехоплює клік і надсилає AJAX-запит на сервер: знімок поточного стану компонента (count: 0) і назву методу (increment);
  2. сервер заново створює компонент зі знімка - між запитами на сервері нічого не зберігається;
  3. викликає increment(), стан стає count: 1;
  4. рендерить Blade-шаблон у HTML і формує новий знімок;
  5. браузер отримує HTML і морфить DOM - змінює лише те, що відрізняється, не перебудовуючи сторінку. Фокус, позиція курсору й стан Alpine зберігаються.

Що з цього випливає:

  • кожна дія - мережевий запит. Миттєвих речей на кшталт розкриття меню через сервер не роблять - для цього є Alpine, вбудований у Livewire;
  • публічні властивості видно в браузері (вони в знімку), а викликати можна будь-який публічний метод - тому дії треба авторизовувати як звичайні HTTP-запити;
  • перший рендер серверний - сторінка приходить готовим HTML, що добре для SEO і швидкості першого показу.

Підключення: у Livewire 4 скрипти й стилі додаються автоматично на сторінки з компонентами. Alpine окремо підключати не треба - він уже в складі Livewire.

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

Livewire 4 підтримує три формати одного й того самого компонента.

1. Однофайловий (SFC) - за замовчуванням:

php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php
<?php

use Livewire\Component;

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

    public function save(): void { /* ... */ }
};
?>

<div>
    <input wire:model="title">
    <button wire:click="save">Зберегти</button>
</div>

PHP-клас (анонімний) і шаблон в одному файлі. Емодзі ⚡ у назві - щоб компоненти було видно в дереві файлів; його можна вимкнути в config/livewire.php (make_command.emoji).

2. Багатофайловий (MFC) - --mfc:

resources/views/components/post/⚡create/
├── create.php          # клас
├── create.blade.php    # шаблон
├── create.js           # JavaScript (необов'язково)
├── create.css          # стилі компонента (необов'язково)
└── create.test.php     # Pest-тест (з --test)

3. Класовий (як у Livewire 3) - --class: клас в app/Livewire/..., шаблон у resources/views/livewire/....

Коли що:

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

Перемикання між форматами - php artisan livewire:convert post.create (з --mfc чи --sfc).

Сторінки зручно тримати окремо: make:livewire pages::post.create створює компонент у resources/views/pages/.

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

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

wire:model зв'язує поле форми з публічною властивістю компонента. Але за замовчуванням не надсилає запит при введенні: значення оновлюється в браузері й потрапляє на сервер разом з наступною дією (wire:click, wire:submit).

<form wire:submit="save">
    <input wire:model="title">          {{-- піде на сервер при відправці форми --}}
    <button type="submit">Зберегти</button>
</form>

Для більшості форм це саме те, що треба: жодного запиту на кожну літеру.

wire:model.live - надсилає оновлення під час введення, з затримкою (debounce) 150 мс: запит іде, коли користувач зупинився.

<input wire:model.live="search">                  {{-- живий пошук --}}
<input wire:model.live.debounce.500ms="search">   {{-- рідше --}}

Модифікатори часу синхронізації в Livewire 4:

Модифікатор Коли оновлюється значення
.live під час введення, з запитом
.blur при втраті фокуса
.change на подію change (зручно для select)
.enter при натисканні Enter
.lazy на change з запитом (сумісність з v3)

Пастка v4: .blur, .change і .enter самі по собі лише оновлюють значення в браузері. Щоб при цьому ще й піти запит на сервер, їх поєднують з .live: wire:model.blur.live="email" - класичний варіант для валідації поля при виході з нього.

Інші модифікатори: .number і .boolean - приведення типу на сервері, .fill - взяти початкове значення з атрибута value в HTML, .throttle.Xms - не частіше, ніж раз на X мс.

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

  • select, що залежить від іншого (міста після області), потребує wire:key на залежному полі, інакше вибране значення не скинеться;
  • у textarea не треба виводити {{ $content }} всередині - значення підставить wire:model;
  • wire:model у Livewire 4 реагує лише на події самого елемента, а не на спливаючі з дочірніх (для старої поведінки - .deep);
  • живе оновлення коштує - кожен запит відтворює компонент і рендерить шаблон. Для полів, що не впливають на інші частини екрана, .live не потрібен.

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

Компонент може бути не лише частиною сторінки, а цілою сторінкою - без контролера.

Маршрут у Livewire 4 - через Route::livewire():

// routes/web.php
Route::livewire('/posts/create', 'pages::post.create');
Route::livewire('/posts/{post}', 'pages::post.show');

Макет. Сторінка рендериться всередині макета - за замовчуванням layouts::app (resources/views/layouts/app.blade.php), де має бути {{ $slot }}. Створити його: php artisan livewire:layout. Змінити глобально - ключ component_layout у config/livewire.php; для окремого компонента - атрибут:

new #[Layout('layouts::dashboard')] class extends Component { /* ... */ };

Заголовок сторінки - атрибут #[Title], а в макеті <title>{{ $title ?? config('app.name') }}</title>:

new #[Title('Нова публікація')] class extends Component { /* ... */ };

Для динамічного заголовка - ->title(...) у методі render():

public function render()
{
    return $this->view()->title("Редагування: {$this->post->title}");
}

Параметри маршруту приходять у mount() за назвою, працює і прив'язка моделей:

new class extends Component {
    public Post $post;

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

Інші слоти макета (наприклад, $lang чи мета-теги) задаються через <x-slot:lang> поза кореневим елементом шаблону компонента.

Що варто пам'ятати:

  • mount() виконується лише під час першого завантаження сторінки. Перевірка доступу в mount() не захищає наступні дії компонента - їх авторизують окремо;
  • middleware маршруту (auth, can:...) застосовуються як до звичайних маршрутів;
  • у Livewire 3 сторінки реєстрували через Route::get('/posts/create', CreatePost::class), а ключ конфігурації називався layout - при оновленні до v4 це змінюється.

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

wire:submit на тегу <form> перехоплює відправку форми й викликає метод компонента замість звичайного POST-запиту з перезавантаженням сторінки.

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

    public function save(): void
    {
        $validated = $this->validate([
            'title' => 'required|min:3',
            'content' => 'required',
        ]);

        auth()->user()->posts()->create($validated);

        $this->redirectRoute('posts.index');
    }
};
<form wire:submit="save">
    <input type="text" wire:model="title">
    <textarea wire:model="content"></textarea>
    <button type="submit">Опублікувати</button>
</form>

Що робить Livewire автоматично:

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

Параметри методу можна передати прямо в директиві: wire:submit="save('draft')".

Чому type="submit" важливий: кнопка без типу всередині форми теж відправляє її, а кнопка «Скасувати» з wire:click усередині форми має бути type="button", інакше натискання на неї відправить форму.

Enter у полі відправляє форму, як у звичайному HTML, - це правильна поведінка для доступності, і її не варто ламати.

Порівняно з контролером: логіка така сама - валідація, авторизація, збереження, редирект, - але без окремого маршруту й з помилками валідації, що з'являються без перезавантаження сторінки. Метод save() - публічна дія, яку можна викликати з браузера будь-коли, тож перевірки прав мають бути всередині нього.

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

Валідація в Livewire побудована на звичайній валідації Laravel - ті самі правила й повідомлення.

Спосіб 1 - $this->validate() з правилами:

public function save(): void
{
    $validated = $this->validate([
        'title' => 'required|min:3|max:255',
        'email' => 'required|email',
    ]);

    Post::create($validated);
}

Якщо дані невалідні, метод кидає виняток, виконання дії зупиняється, а помилки повертаються в браузер. validate() повертає лише перевірені поля - їх безпечно передавати в create().

Спосіб 2 - атрибут #[Validate] над властивістю:

use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|min:3')]
    public string $title = '';

    #[Validate('required|email', as: 'електронна адреса')]
    public string $email = '';

    public function save(): void
    {
        $this->validate();   // правила беруться з атрибутів
    }
};

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

Показ помилок - звичайні директиви Blade:

<input wire:model="title" @error('title') aria-invalid="true" @enderror>
@error('title')
    <p class="text-sm text-red-600">{{ $message }}</p>
@enderror

Важливо: $this->validate() перед збереженням потрібен навіть з #[Validate] - атрибут перевіряє лише поля, які оновлювалися. Поле, якого користувач не торкався, без явного validate() залишиться неперевіреним.

Обмеження атрибутів: у PHP-атрибутах не можна використовувати об'єкти правил (Rule::unique(...), Password::defaults()). Для них - метод rules().

Українські повідомлення - через файли перекладів lang/uk/validation.php, як і в усьому Laravel; назву поля задає параметр as: чи метод validationAttributes().

Ручне керування помилками: $this->addError('email', 'Ця адреса вже зайнята'), $this->resetValidation('email').

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

Livewire відстежує, чи відрізняється стан у браузері від стану на сервері. Форма, в якій користувач щось змінив, але це ще не відправлено, вважається «брудною» (dirty).

wire:dirty показує елемент лише тоді, коли є незбережені зміни:

<form wire:submit="update">
    <input wire:model="title">
    <textarea wire:model="content"></textarea>

    <div wire:dirty class="text-amber-600">Є незбережені зміни</div>

    <button type="submit">Зберегти</button>
</form>

Щойно користувач почав вводити - повідомлення з'являється; після відправки форми й синхронізації з сервером - зникає.

Варіанти:

  • wire:dirty.remove - навпаки: показувати елемент, доки змін немає («Усе збережено»);
  • wire:dirty.class="border-amber-500" - додати клас замість показу елемента;
  • wire:target="title" - стежити лише за конкретною властивістю:
<input wire:model.live.blur="title" wire:dirty.class="border-amber-500" wire:target="title">
  • $dirty у виразах і $wire.$dirty('email') в Alpine - перевірити програмно:
<button x-bind:disabled="! $wire.$dirty()">Зберегти</button>

Кнопка «Зберегти» активна лише тоді, коли є що зберігати.

Що важливо розуміти: «чисто» означає «синхронізовано із сервером», а не «збережено в базу». Якщо поле з wire:model.live уже відправило значення на сервер, а метод збереження ще не викликано, wire:dirty уже не покаже змін. Для форм, де важливо саме збереження в базу, стан «змінено» краще вести явно (наприклад, порівнювати з початковими значеннями).

Корисне доповнення: попередження при спробі піти зі сторінки з незбереженими змінами - обробник beforeunload в Alpine, що перевіряє $wire.$dirty().

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

wire:confirm показує діалог підтвердження перед виконанням дії. Якщо користувач натиснув «Скасувати» чи Escape, запит на сервер не надсилається.

<button
    type="button"
    wire:click="delete({{ $post->id }})"
    wire:confirm="Видалити цей пост? Дію не можна скасувати."
>
    Видалити
</button>

Працює з будь-якою дією: wire:click, wire:submit тощо.

Підтвердження введенням тексту - для особливо руйнівних дій (видалення облікового запису чи проєкту). Модифікатор .prompt, очікуваний текст - після |:

<button
    wire:click="deleteAccount"
    wire:confirm.prompt="Щоб підтвердити, введіть ВИДАЛИТИ|ВИДАЛИТИ"
>
    Видалити обліковий запис
</button>

Дія виконається лише якщо введено точно ВИДАЛИТИ (з урахуванням регістру).

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

  • це стандартні діалоги браузера (confirm() / prompt()): виглядають по-різному в різних браузерах і не стилізуються. Для фірмового вигляду роблять власне модальне вікно (Alpine чи готові компоненти на кшталт Flux), а дію викликають з кнопки всередині нього;
  • підтвердження - це UX, а не захист. Метод delete() можна викликати напряму з консолі браузера ($wire.delete(5)), минаючи діалог. Перевірка прав і власності запису обов'язкова в самому методі:
public function delete(Post $post): void
{
    $this->authorize('delete', $post);
    $post->delete();
}
  • для масових чи незворотних операцій варто подумати про «м'яке» видалення (SoftDeletes) і можливість відновити - це надійніше за будь-які діалоги;
  • текст діалогу - звичайний рядок, тож його варто перекладати так само, як решту інтерфейсу (wire:confirm="{{ __('...') }}").

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

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

Проблема зі списками. Без підказок морфінг зіставляє елементи за позицією. Видалили перший елемент списку - і алгоритм вирішує, що змінився вміст кожного елемента, а останній зник:

@foreach ($todos as $todo)
    <li>
        <input type="checkbox" wire:model="done.{{ $todo->id }}">
        <span x-data="{ editing: false }">{{ $todo->title }}</span>
    </li>
@endforeach

Наслідки: стан Alpine (editing), фокус і введення «переїжджають» на сусідні рядки, анімації спрацьовують не там, а вкладені Livewire-компоненти отримують чужі дані.

wire:key дає кожному елементу стабільний ідентифікатор - морфінг зіставляє елементи за ключем:

@foreach ($todos as $todo)
    <li wire:key="todo-{{ $todo->id }}">...</li>
@endforeach

@foreach ($posts as $post)
    <livewire:post-card :$post :wire:key="$post->id" />
@endforeach

Правила:

  • ключ - ідентифікатор даних (id), а не індекс циклу. $loop->index не допомагає: після видалення елемента індекси зсуваються так само;
  • ключ має бути унікальним у межах компонента. Якщо на сторінці два списки з тих самих моделей, додайте префікс: "comment-{{ $id }}", "reply-{{ $id }}";
  • для вкладених Livewire-компонентів у циклі ключ обов'язковий - без нього стан дочірніх компонентів змішується.

Налаштування smart_wire_keys (у Livewire 4 увімкнено за замовчуванням) допомагає з ключами глибоко вкладених компонентів, але не звільняє від wire:key у циклах.

Ще один випадок - залежні поля: <select> міст, що змінюється разом з обраною областю, потребує wire:key="{{ $regionId }}", щоб значення коректно скинулося.

Альтернатива морфінгу для складних випадків - wire:replace: замінити вміст елемента повністю замість порівняння.

Докладніше в документації: Морфінг

wire:poll періодично надсилає запит до компонента й оновлює його - найпростіший спосіб показувати свіжі дані без WebSocket.

<div wire:poll>
    Нових замовлень: {{ $this->newOrdersCount }}
</div>

<div wire:poll.15s="refreshStatus">
    Статус імпорту: {{ $status }}
</div>

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

Чому це дорого: тисяча відкритих вкладок з інтервалом 2,5 с - 400 запитів на секунду на сервер, кожен з гідрацією компонента, запитами до бази й рендером. Для PHP-FPM це сотні зайнятих процесів лише на опитування.

Як зменшити навантаження:

  • довший інтервал. Найефективніший і найпростіший крок: .10s, .1m замість типових 2,5 с;
  • .visible - опитувати лише тоді, коли елемент видно на екрані. Блок унизу довгої сторінки не опитується, поки до нього не прокрутять;
  • фонові вкладки Livewire пригальмовує автоматично - на 95%. Модифікатор .keep-alive вимикає це, і без справжньої потреби його не варто ставити;
  • легкий метод опитування: перевірити дешеву ознаку змін (кількість, max(updated_at)) і не перебудовувати важкі дані, якщо нічого не змінилося;
  • острівець (@island) з опитуванням - оновлюється лише ця частина компонента, а не весь шаблон;
  • #[Isolate] - щоб повільне опитування одного компонента не затримувало запити інших.

Livewire 4: опитування більше не блокує інші дії - клік користувача не чекає на запит опитування в черзі.

Коли опитування - неправильний інструмент:

  • оновлення потрібні миттєво (чат, спільне редагування) - WebSocket (Laravel Reverb + Echo), і Livewire слухає події трансляції через #[On('echo:...')];
  • оновлення рідкісні, а відвідувачів багато - теж краще події, ніж тисячі порожніх запитів;
  • зміна станеться один раз (завершення імпорту) - опитування з зупинкою: перестаньте рендерити wire:poll, коли статус фінальний.

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

Компонент з повільним запитом (статистика, звіт, зовнішній API) затримує всю сторінку: сервер не відправить HTML, поки не виконає mount() кожного компонента.

Два способи відкласти:

<livewire:revenue-chart lazy />    {{-- коли стане видимим на екрані --}}
<livewire:revenue-chart defer />   {{-- одразу після завантаження сторінки --}}
  • lazy - компонент завантажується, коли потрапляє в область видимості (прокрутили до нього). Для блоків унизу сторінки, які можуть і не знадобитися;
  • defer - завантажується одразу після першого показу сторінки, незалежно від прокрутки. Для того, що потрібно завжди, але не повинно затримувати першу відповідь.

Сторінка приходить швидко з заглушкою на місці компонента, а сам компонент підвантажується окремим запитом.

Заглушка (placeholder) - що бачить користувач, поки компонент вантажиться:

@placeholder
    <div class="h-64 animate-pulse rounded bg-gray-100"></div>
@endplaceholder

<div>
    Дохід цього місяця: {{ $amount }}
</div>

У класових компонентах - метод placeholder(). Кореневий тег заглушки й компонента має збігатися (<div> і <div>). Добра заглушка має той самий розмір, що й готовий блок, - інакше сторінка «стрибатиме».

Зробити відкладеним завжди - атрибути на класі #[Lazy] чи #[Defer]. Для цілої сторінки - Route::livewire(...)->lazy().

Об'єднання запитів. За замовчуванням кожен відкладений компонент вантажиться окремим паралельним запитом. Десять віджетів на дашборді - десять запитів. Можна об'єднати в один:

<livewire:revenue lazy.bundle />

Але тоді повільний віджет затримає швидкі - об'єднання доречне для схожих за швидкістю компонентів.

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

  • параметри, передані відкладеному компоненту (:user="$user"), серіалізуються й чекають у браузері до завантаження - тож моделі в них перезавантажуються з бази;
  • SEO: вміст відкладеного компонента відсутній у початковому HTML;
  • у тестах - Livewire::withoutLazyLoading(), щоб бачити справжній вміст, а не заглушку;
  • для частини компонента, а не цілого компонента, - острівець @island(lazy: true).

Докладніше в документації: Ліниве завантаження

Обчислювана властивість - метод з атрибутом #[Computed], до якого звертаються як до властивості. Результат кешується в межах одного запиту.

use Livewire\Attributes\Computed;

new class extends Component {
    public string $search = '';

    #[Computed]
    public function users()
    {
        return User::query()
            ->where('name', 'like', "%{$this->search}%")
            ->with('team')
            ->limit(20)
            ->get();
    }
};
@foreach ($this->users as $user)
    <li wire:key="{{ $user->id }}">{{ $user->name }} ({{ $user->team->name }})</li>
@endforeach
<p>Знайдено: {{ $this->users->count() }}</p>   {{-- повторний доступ - без нового запиту --}}

У шаблоні - лише через $this->: $this->users, а не $users.

Чим краще за публічну властивість (public $users заповнена в mount()):

  • не потрапляє в знімок стану - не їде в браузер і назад з кожним запитом;
  • не перезавантажується з бази на кожен запит гідрації - лише коли шаблон чи код справді звертається до неї;
  • завантажені зв'язки (with('team')) працюють, бо запит виконується повністю щоразу, а не відновлюється за ключами;
  • завжди актуальна - похідна від поточного стану ($search), її не треба оновлювати вручну.

Ліниве виконання - головна перевага: якщо дані потрібні лише в гілці @if, запит виконається лише тоді, коли умова істинна. А в острівці - лише коли рендериться саме острівець.

Скинути кеш у межах запиту, якщо дані змінилися:

public function addUser(): void
{
    User::create([...]);
    unset($this->users);   // наступне звернення виконає запит заново
}

Кешування між запитами - #[Computed(persist: true)] (на компонент, година за замовчуванням) чи #[Computed(cache: true)] (спільно для всіх екземплярів) - але з обережністю щодо персональних даних.

Правило: публічні властивості - для стану, який змінює користувач (фільтри, введені значення, обраний id). Дані, що з них виводяться, - обчислювані властивості.

Докладніше в документації: Обчислювані властивості

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

Відправити подію з 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), щоб не зіткнутися з подіями сторонніх бібліотек.

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

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

Інші рівні
Middle 41 Senior 36

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