<? phpukraine СТАТТІ
Пошук по платформі
LARAVEL 15 вересня 2026 · 7 хв читання

Livewire 4: де він доречний, а де краще звичайний запит або API

На phpukraine дев'ять Livewire-компонентів на весь портал: каталог вакансій, редактор статей, форми реєстрації й ліди. Стрічки статей, документації та компаній живуть на звичайних контролерах із soft-навігацією, а інструменти в /tools ходять у JSON-ендпоїнт. Розбираємо, за яким критерієм вибирається кожен із трьох варіантів і скільки коштує помилитись.

РP
Редакція phpukraine
Редакція платформи

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

На порталі зараз дев'ять компонентів у app/Livewire: каталог вакансій, редактор статей, редактор профілю розробника, форма вакансії для роботодавця, дві форми реєстрації, дії з вакансією, контактна форма й форма ліда. Решта сайту тримається на контролерах, Blade і невеликому перехоплювачі кліків у resources/js/app.js. Далі про критерій, за яким проходить цей розподіл.

Питання, на яке насправді відповідає Livewire

Livewire платить за кожну взаємодію мережевим запитом і повним рендером компонента на сервері. Взамін дає одну річ: стан живе в PHP, і вам не треба тримати його копію в браузері.

Тому питання перед новим компонентом одне: чи є на екрані стан, який мусить пережити клік, і чи дорого відтворювати наслідок цього стану на клієнті?

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

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

Каталог вакансій: лічильники, які знає тільки база

Панель фільтрів у каталозі має шість груп (фреймворк, рівень, місто, формат, тип зайнятості, англійська), поріг зарплати, перемикач сортування й пагінацію по 20. Біля кожної опції стоїть кількість вакансій, яка перераховується після кожного кліку, бо фасети рахуються з урахуванням уже вибраних фільтрів. Плюс лендинг має власний пресет, який не можна зняти: на /jobs/laravel фреймворк зафіксований.

Відтворити це на клієнті означає віддати браузеру всі вакансії з усіма полями й переписати фасетний підрахунок на JavaScript. Друга реалізація тих самих правил стає другим місцем, де вони розійдуться.

Компонент тримає стан у публічних властивостях, а #[Url] синхронізує їх із query string, тому будь-який набір фільтрів лишається посиланням, яке можна надіслати:

class JobsCatalog extends Component
{
    public const PER_PAGE = 20;

    #[Locked]
    public string $landingPath = '';

    #[Url(as: 'framework', except: '')]
    public string $framework = '';

    #[Url(as: 'with_salary', except: false)]
    public bool $withSalary = false;

    public function toggle(string $group, string $value): void
    {
        $current = $this->list($group);

        $this->{$group} = implode(',', in_array($value, $current, true)
            ? array_values(array_diff($current, [$value]))
            : [...$current, $value]);

        $this->page = 1;
    }
}

#[Locked] тут не косметика. landingPath визначає пресет, а отже й те, які опції заблоковані; без атрибута його можна було б підмінити з фронтенду разом зі снапшотом.

Перший рендер серверний, з mount(), який читає initialQuery з контролера. Краулер отримує готову сторінку з результатами, а не порожній каркас, що дозаповниться після XHR. Вкладки пресетів лишаються справжніми посиланнями, і вже поверх них висить перехоплення:

<a href="{{ $tab->url() }}" wire:click.prevent="switchLanding('{{ $tab->path }}')">{{ $tab->crumb }}</a>

Метод усередині чистить фільтри, змінює лендинг і сам править історію та заголовок через $this->js(). Без JavaScript це звичайний перехід на іншу сторінку; з ним заміна списку без перезавантаження. Обидві гілки ведуть на однаковий індексований URL.

І одразу про ціну. Кожен клік по фільтру коштує запиту, повного рендеру компонента, вибірки сторінки результатів, перерахунку фасетів і ще одного підрахунку для рішення про індексацію. Тому поля з вільним введенням прив'язані тільки із затримкою: від wire:model.live.debounce.300ms у формах реєстрації до 1500 мс на тілі статті в редакторі. У Livewire 4 з'явилися islands для часткового оновлення, і якщо панель фільтрів колись почне гальмувати саме через розмір рендеру, це наступний крок. Поки дешевше не рендерити зайвого.

Редактор статей: автосейв поверх доменних правил

/write стає другим очевидним кандидатом. Автор пише Markdown, збоку живе чекліст готовності, кнопка «На редактуру» відкривається на 100%. Чернетка зберігається на сервер на кожну зміну.

Livewire знімає тут найнуднішу частину: не потрібен ані окремий ендпоїнт для автосейву, ані серіалізація форми руками. Хук updated() ловить будь-яку зміну властивості й вирішує, що з нею робити:

public function updated(string $name): void
{
    if (in_array($name, ['view', 'savedAt', 'error'], true)) {
        return;
    }

    if ($name === 'kind' && ArticleKind::tryFrom($this->kind)?->requiresOrigin() === true) {
        $this->mode = 'blog';
    }

    $this->save();
}

Межа проходить у submit(). Компонент не вирішує, чи стаття готова до редактури, він викликає доменний метод і показує помилку, якщо той відмовився:

try {
    $submission->submit(app(Clock::class)->now());
} catch (\InvalidArgumentException $e) {
    $this->error = $e->getMessage();

    return;
}

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

arch('the web layer never touches infrastructure directly')
    ->expect(['App\Http', 'App\Livewire'])
    ->not->toUse(['PhpUkraine\Hiring\Infrastructure', 'PhpUkraine\ContentOps\Infrastructure']);

Домен і Application, своєю чергою, не мають права згадувати Livewire взагалі. Звідси приємний побічний ефект: Livewire::test(ArticleEditor::class) ганяє сценарій редактора, а правила публікації тестуються без жодного HTTP.

Стрічки: контролер, Blade і заміна <main>

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

Щоб переходи не виглядали як перезавантаження, у resources/js/app.js лежить маленький перехоплювач кліків. Він працює тільки всередині секції (посилання з /articles/* на /articles/*), тягне сторінку фетчем і міняє <main>:

const res = await fetch(url, { headers: { 'X-Requested-With': 'soft' } });
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
const next = doc.querySelector('main');
if (!next || next.querySelector('[wire\\:id]')) { location.href = url; return; }
target.replaceWith(next);
document.title = doc.title;

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

Кожен URL лишається справжньою сторінкою: відкритий напряму, він рендериться сервером повністю, history.pushState зберігає позицію прокрутки, popstate її повертає. Витрати: один fetch на перехід і жодного стану на сервері. Замінити це каталогом на Livewire означало б перетворити десятки індексованих сторінок на один компонент із параметрами.

Коли краще JSON-ендпоїнт

У /tools є тестер регулярних виразів, парсер cron, перевірка синтаксису й генератор хешів. Ввід змінюється на кожен символ, і результат треба показувати миттєво. Livewire дав би тут по запиту на натиск клавіші заради перемальовування всієї сторінки.

Розв'язок звичайний для такого класу задач: розбір і підсвітка живуть у JS-чанку, який підвантажується лише на сторінці інструмента, а те, що вміє тільки PHP, іде окремим викликом:

public function __invoke(Request $request, string $slug): JsonResponse
{
    $class = self::TOOLS[$slug] ?? throw new NotFoundHttpException;

    return response()->json(['ok' => true, 'result' => app($class)->run($request->json()->all())])
        ->header('Cache-Control', 'no-store');
}

Той самий критерій діє для форми відгуку на вакансію. Вона свідомо зроблена звичайним POST з $request->validate(), honeypot-полем і throttle на маршруті. Стану між кліками в ній немає, вона має працювати без JavaScript, а окремий компонент означав би Livewire на кожній сторінці вакансії заради одного сабміту.

Що з цього виходить як правило

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

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

Найбільше економить організаційне правило: компонент не тримає доменної логіки. Тоді питання «Livewire чи контролер» лишається питанням про транспорт, і змінити відповідь пізніше коштує рівно стільки, скільки переписати транспорт.

ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
РP
Редакція phpukraine
Редакція платформи
Матеріали, які готує команда платформи на основі власних даних: каталогу вакансій, зарплатного звіту й банку питань. Кожна цифра в них рахується з бази, а не береться з голови.
оновлено 15 вересня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті