Питання на співбесіді: Next.js, Inertia й маршрутизація
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
12 питань
Є три поширені архітектури, і вони відрізняються тим, хто відповідає за маршрутизацію й дані.
1. Окрема SPA + API Laravel (Vite + React Router, Laravel лише як JSON API, автентифікація через Sanctum):
- фронтенд і бекенд незалежні: окремі деплої, можна мати кілька клієнтів (веб, мобільний застосунок);
- треба самому будувати API, валідацію відповідей, обробку помилок, стан завантаження, маршрутизацію й автентифікацію (CORS, CSRF-cookie);
- без SSR сторінки погано індексуються - для публічного контенту це мінус.
2. Inertia.js (React-сторінки, але маршрути й контролери - Laravel):
return Inertia::render('Orders/Show', ['order' => $order->only('id', 'total', 'status')]);
export default function Show({ order }) { /* ... */ }
- немає окремого API: контролер передає дані як props сторінки, валідація, авторизація, редиректи, сесії - звичайний Laravel;
- переходи без перезавантаження, як у SPA;
- SSR можливий (окремий Node-процес), але не обов'язковий;
- один застосунок - простіше для невеликої команди. Офіційний стартовий набір Laravel для React побудований саме так.
3. Next.js + Laravel як API:
- React Server Components, потоковий рендер, кешування на рівні фреймворка, SSR/SSG з коробки - найкраще для SEO й публічних сайтів з великим трафіком;
- два бекенди: Node-сервер Next.js і Laravel. Автентифікація, кешування й деплой стають складнішими;
- частина логіки (серверні дії, маршрути) переїжджає з Laravel у Next.js - треба вирішити, де межа.
Як обирати:
| Ситуація | Вибір |
|---|---|
| адмінка, кабінет, внутрішній інструмент на Laravel | Inertia |
| кілька клієнтів одного API (веб + мобільний) | SPA + API |
| публічний контент-сайт з вимогами до SEO й швидкості | Next.js (або Inertia з SSR) |
| команда з окремими фронтенд- і бекенд-розробниками | SPA чи Next.js + API |
Головне питання: чи потрібен окремий API як продукт. Якщо ні - Inertia дає SPA-досвід без витрат на API.
React Router має три режими, і кожен наступний додає можливості поверх попереднього.
1. Декларативний - маршрути описано в JSX, лише зіставлення URL з компонентами й навігація:
import { BrowserRouter, Routes, Route, Link } from 'react-router';
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:id" element={<Post />} />
</Routes>
</BrowserRouter>
Хуки useParams, useNavigate, useLocation, компонент <Link>. Дані завантажує сам компонент.
2. Режим даних - маршрути описано об'єктами поза рендером, і до них додаються завантажувачі даних і дії:
const router = createBrowserRouter([
{ path: '/posts/:id', Component: Post, loader: ({ params }) => getPost(params.id) },
]);
<RouterProvider router={router} />
Дані завантажуються до рендеру сторінки, паралельно для вкладених маршрутів, - без «водоспаду» запитів у useEffect. Дії (action) обробляють форми, після чого дані сторінки перевантажуються автоматично.
3. Фреймворковий - режим даних плюс плагін Vite: типобезпечні модулі маршрутів, автоматичне розділення коду, SSR, статичний рендер чи SPA на вибір. По суті це наступник Remix.
Як обирати: документація радить фреймворковий режим тим, хто не має сильної думки, - він дає найбільше. Декларативний - для простих застосунків або поступового переходу; режим даних - коли потрібні завантажувачі, але власна збірка.
React Router 8 (червень 2026) - перший реліз за щорічним графіком мажорних версій:
- пакет
react-router-domвидалено - усі імпорти зreact-router(іreact-router/dom); - мінімальні версії: React 19.2.7+, Node 22.22+, Vite 7+; пакети тепер лише ESM;
- поведінка прапорців
future.v8_*стала типовою: middleware маршрутів увімкнено завжди, модулі маршрутів розділяються на частини за замовчуванням; - у
meta-функціях замість застарілогоdata-loaderData.
Якщо у v7 всі прапорці майбутньої версії вже були увімкнені, перехід на v8 мінімальний.
З Laravel React Router доречний в окремій SPA. В Inertia маршрутизація належить Laravel, і React Router не потрібен.
У App Router маршрути визначає структура папок у app/, а спеціальні файли всередині папки задають роль.
app/
├── layout.tsx # кореневий макет (обов'язковий, містить <html> і <body>)
├── page.tsx # /
├── blog/
│ ├── layout.tsx # макет для всіх сторінок /blog/*
│ ├── page.tsx # /blog
│ └── [slug]/
│ ├── page.tsx # /blog/:slug
│ ├── loading.tsx # стан завантаження
│ └── error.tsx # межа помилок
page.tsx - вміст маршруту. Без нього папка не стає доступним URL.
layout.tsx - спільна обгортка для сторінки й усіх вкладених маршрутів:
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<BlogSidebar />
{children}
</section>
);
}
Ключова властивість макетів: при переході між сторінками всередині макета він не перемонтовується - зберігає стан (відкрита бічна панель, введений пошук) і не рендериться заново. Перемальовується лише змінена частина.
Динамічні сегменти: [slug] - параметр; params у сторінці - Promise, його треба дочекатися:
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
Інші файли:
loading.tsx- автоматичний<Suspense>: запасний вигляд, поки сторінка завантажує дані;error.tsx- межа помилок сегмента (має бути клієнтським компонентом);not-found.tsx- дляnotFound();route.ts- обробник HTTP-запитів (API) замість сторінки.
Групи маршрутів (marketing) - папки в дужках не потрапляють в URL і дають змогу мати різні макети для частин сайту.
За замовчуванням усі ці компоненти - серверні: можна звертатися до бази чи API прямо в компоненті, а 'use client' додається лише там, де потрібна інтерактивність.
Next.js читає .env-файли й робить змінні доступними через process.env. Але де саме вони доступні, залежить від префікса.
Без префікса - лише на сервері:
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...
Доступні в серверних компонентах, обробниках маршрутів, серверних функціях. У браузері process.env.STRIPE_SECRET_KEY - undefined.
З префіксом NEXT_PUBLIC_ - у браузері теж:
NEXT_PUBLIC_ANALYTICS_ID=G-XXXX
NEXT_PUBLIC_API_URL=https://api.example.com
setupAnalytics(process.env.NEXT_PUBLIC_ANALYTICS_ID);
Як це працює: під час next build Next.js підставляє значення прямо в код JavaScript, що йде в браузер. Тобто:
- значення публічне - будь-хто побачить його в коді сторінки. Сюди можна класти лише те, що й так не секретно;
- значення фіксується на момент збирання. Змінити
NEXT_PUBLIC_API_URLна сервері після збірки - нічого не змінить у вже зібраних файлах. Один Docker-образ для staging і production з різнимиNEXT_PUBLIC_*не працюватиме як очікується; - динамічне звернення не підставляється:
process.env[name]чи деструктуризаціяconst { NEXT_PUBLIC_X } = process.envдадутьundefinedу браузері.
Як передати конфігурацію під час виконання, якщо образ один на всі середовища:
- читати змінні без префікса в серверному компоненті й передавати потрібні значення в клієнтські компоненти як props;
- або ендпойнт конфігурації, з якого клієнт отримує налаштування.
Порядок файлів: .env.local (не в Git, перекриває все), .env.development / .env.production, .env. Змінні оточення процесу мають пріоритет над файлами.
Пастки безпеки:
- випадково додати
NEXT_PUBLIC_до секрету - класичний витік ключа API; - серверний модуль з секретами, імпортований у клієнтський компонент, потрапляє в бандл - захист: пакет
server-onlyу таких модулях дає помилку збірки при імпорті з клієнта.
Аналог у Vite (Laravel-проєкти) - префікс VITE_ і import.meta.env, з тими самими правилами: усе з префіксом публічне й фіксується при збиранні.
У App Router компоненти за замовчуванням серверні. Директива 'use client' на початку файлу позначає межу: цей модуль і все, що він імпортує, потрапляє в клієнтський бандл.
// app/products/[id]/page.tsx - серверний компонент
import AddToCart from './add-to-cart';
export default async function Page({ params }) {
const { id } = await params;
const product = await db.product.find(id); // запит прямо в компоненті
return (
<>
<h1>{product.name}</h1>
<AddToCart productId={product.id} price={product.price} />
</>
);
}
// add-to-cart.tsx
'use client';
export default function AddToCart({ productId, price }) {
const [qty, setQty] = useState(1);
return <button onClick={() => addToCart(productId, qty)}>У кошик - {price * qty} грн</button>;
}
Що можна передати з серверного в клієнтський компонент - лише серіалізовані значення:
- примітиви, звичайні об'єкти й масиви,
Date,Map,Set, типізовані масиви; - Promise (клієнт розгорне через
use()); - React-елементи (JSX), зокрема
children; - серверні функції (
'use server').
Не можна: звичайні функції (обробники подій), екземпляри класів, моделі ORM з методами, символи (крім глобально зареєстрованих).
Важливі наслідки:
- усе передане видно в браузері. Передати в клієнтський компонент повний об'єкт користувача з бази - відправити в HTML і хеш пароля, і службові поля. Передавайте лише потрібні поля;
- межу ставлять якомога нижче - на інтерактивний «листок» (кнопка, форма), а не на всю сторінку. Інакше весь вміст і його залежності йдуть у бандл;
- серверний компонент можна вставити в клієнтський через
children, але не імпортувати в клієнтський модуль напряму - імпорт перетворить його на клієнтський; - контекст і провайдери (
ThemeProvider) - клієнтські компоненти, що обгортаютьchildrenу макеті; - бібліотеки без
'use client', що використовують хуки, треба обгорнути у власний клієнтський модуль.
'use client' - не «рендер лише в браузері». Клієнтські компоненти теж рендеряться на сервері при першому завантаженні (SSR), а потім гідруються. Тому в їхньому рендері не можна звертатися до window без перевірок.
Докладніше в документації: Next.js: серверні й клієнтські компоненти
Модель кешування в Next.js змінювалася між версіями, і це найчастіше джерело плутанини на співбесідах.
Що відбувається з fetch зараз:
- запити не кешуються за замовчуванням (так з Next.js 15; у 13-14 було навпаки) - кожен запит до сторінки отримує свіжі дані;
- однакові
fetchв одному рендері мемоізуються - можна запитувати дані в кожному компоненті, якому вони потрібні, і запит піде один раз. Мемоізація діє лише в межах одного запиту до сервера; - для власних функцій доступу до бази таку мемоізацію дає
React.cache.
Cache Components (Next.js 16) - нова модель, що вмикається в конфігурації:
// next.config.ts
const nextConfig = { cacheComponents: true };
Тоді кешування задається директивою 'use cache' - на рівні функції з даними чи цілого компонента:
import { cacheLife, cacheTag } from 'next/cache';
export async function getProducts(category: string) {
'use cache';
cacheLife('hours');
cacheTag('products');
return db.product.findMany({ where: { category } });
}
- аргументи стають частиною ключа кешу - різні категорії кешуються окремо;
cacheLifeзадає тривалість (документація радить додавати її до кожної директиви);cacheTag+revalidateTag('products')- точкове скидання кешу після зміни даних, наприклад у серверній функції після збереження товару;- кешовані частини стають статичною «оболонкою» сторінки, а некешовані - рендеряться під час запиту всередині
<Suspense>і надходять потоком.
Пастки:
- персональні дані в кеші: функція з
'use cache', що читає дані поточного користувача, спільна для всіх, якщо користувач не є частиною ключа. Значення зcookies()/headers()всередині кешованої області використовувати не можна - їх передають аргументами; - застарілі дані після змін - без
revalidateTag/revalidatePathу місці зміни користувачі бачитимуть старе до кінця терміну; - документація для старої моделі (опції
fetch{ next: { revalidate } },export const revalidate) досі трапляється - вона стосується режиму без Cache Components.
Порівняно з Laravel: це аналог Cache::remember з тегами, але вбудований у рендер сторінок.
Деякі дані потрібні на кожній сторінці: поточний користувач для шапки, назва застосунку, флеш-повідомлення, налаштування. Передавати їх у кожному контролері незручно - для цього є спільні дані (shared data).
На сервері - middleware HandleInertiaRequests, яке створює адаптер Laravel:
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'appName' => config('app.name'),
'auth.user' => fn () => $request->user()?->only('id', 'name', 'avatar_url'),
'can' => fn () => [
'manageUsers' => $request->user()?->can('manage-users') ?? false,
],
]);
}
}
На клієнті - usePage():
import { usePage } from '@inertiajs/react';
export default function Header() {
const { auth, appName } = usePage().props;
return <header>{appName} · {auth.user?.name}</header>;
}
Спільні дані зливаються з props сторінки - назви варто групувати (auth.user, can.*), щоб не перетнутися з props конкретної сторінки.
Що видно в браузері - усе. Props сторінки, включно зі спільними, вбудовуються в HTML першої сторінки й приходять у JSON при кожному переході. Будь-хто може відкрити DevTools і прочитати їх. Звідси правила:
- передавати лише потрібні поля:
$user->only('id', 'name'), а не$request->user()цілком (там можуть бути email, телефон, службові прапорці, токени); - не класти секрети - ключі API, внутрішні налаштування;
- прапорці прав (
can.*) - для відображення, а не для захисту. Сховати кнопку «Видалити» можна за прапорцем, але контролер видалення перевіряє права сам.
Продуктивність:
- спільні дані обчислюються й передаються з кожною відповіддю - документація радить використовувати їх ощадливо;
- замикання (
fn () => ...) обчислюються лише тоді, коли prop справді потрібен (наприклад, не при частковому перезавантаженні інших props); Inertia::once()/shareOnce()(Inertia v3) - для даних, що не змінюються між переходами (довідник країн): клієнт отримує їх один раз і пам'ятає.
Флеш-повідомлення в Inertia v3 мають окремий механізм Inertia::flash() - їх не потрібно вручну додавати в спільні дані.
У режимі даних маршрут описується об'єктом, до якого прив'язані завантажувач (loader) для читання й дія (action) для змін.
import { createBrowserRouter, RouterProvider, useLoaderData, Form, useNavigation } from 'react-router';
const router = createBrowserRouter([
{
path: '/projects/:projectId',
Component: Project,
loader: async ({ params }) => api.getProject(params.projectId),
action: async ({ request, params }) => {
const formData = await request.formData();
return api.updateProject(params.projectId, { title: formData.get('title') });
},
},
]);
function Project() {
const project = useLoaderData();
const navigation = useNavigation();
return (
<Form method="post">
<input name="title" defaultValue={project.title} />
<button disabled={navigation.state === 'submitting'}>Зберегти</button>
</Form>
);
}
Завантажувачі:
- виконуються до рендеру сторінки - компонент отримує готові дані, без станів «завантаження» в
useEffect; - для вкладених маршрутів завантажуються паралельно, а не водоспадом батько → дитина;
- помилка в завантажувачі показує
ErrorBoundaryмаршруту.
Дії:
- викликаються через
<Form method="post">,useSubmitабоfetcher; - після дії всі завантажувачі сторінки перевантажуються автоматично - інтерфейс синхронізований з сервером без ручного оновлення стану;
<Form>створює навігацію (новий запис в історії), аuseFetcher- відправляє без навігації: лайки, позначки «прочитано», дії в рядках таблиці.
Стан очікування: useNavigation() (для навігації й форм) і fetcher.state (для фетчерів) - idle, loading, submitting.
У React Router 8 middleware маршрутів увімкнено завжди: спільна логіка (перевірка автентифікації, контекст) виконується до завантажувачів і дій.
Порівняно з Inertia + Laravel: ідея близька - дані «належать» маршруту, а після мутації сторінка оновлюється. Різниця в тому, що в React Router завантажувачі працюють у браузері (і звертаються до API), а в Inertia ту саму роль виконує контролер Laravel на сервері.
Пастка: дія в режимі даних виконується в браузері - це не серверний код. Валідація й авторизація все одно мають бути в API, куди вона звертається.
Серверна функція з 'use server' виглядає як звичайна функція, яку викликає компонент. Насправді Next.js створює для неї ендпойнт, доступний прямим POST-запитом - не лише з вашого інтерфейсу.
'use server';
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } }); // будь-хто видалить будь-який пост
}
Що Next.js робить для захисту сам:
- зашифровані недетерміновані ідентифікатори дій, що змінюються між збірками;
- видалення невикористаних дій з клієнтського бандла;
- шифрування змінних із замикань (значень, захоплених функцією, оголошеною всередині компонента) - вони ходять клієнтом і назад, але в зашифрованому вигляді. Ключ генерується на кожну збірку (для кількох серверів - спільний
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY); - перевірка джерела: заголовок
Originпорівнюється зHost- захист від CSRF; для проксі-конфігурацій -serverActions.allowedOrigins.
Але документація прямо застерігає: це зменшує ризики, а автентифікацію й авторизацію треба перевіряти всередині кожної дії.
Правильно:
'use server';
export async function deletePost(postId: string) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const post = await db.post.findUnique({ where: { id: postId } });
if (!post || post.authorId !== session.user.id) throw new Error('Forbidden'); // захист від IDOR
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
Типові помилки:
- перевірка лише на сторінці: сторінка доступна лише адміну, тож «дії на ній безпечні». Ні - перевірка доступу до сторінки не поширюється на дії, оголошені в ній;
- довіра аргументам:
postId, ціна, роль приходять від клієнта й можуть бути будь-якими. Валідація (Zod) обов'язкова, як для будь-якого API; - повернення зайвого: повернене значення потрапляє в браузер - лише потрібні поля;
- покладатися на шифрування замикань для секретів - документація не радить; секрети не повинні опинитися в замиканні взагалі.
Рекомендований підхід - шар доступу до даних: модуль з import 'server-only', де зібрано перевірки прав і запити до бази. Серверні дії лишаються тонкими й викликають його. Так перевірки не залежать від того, звідки викликано код.
Аналогія з Laravel: серверна дія - це маршрут контролера. Ніхто не робить контролер без authorize() лише тому, що на нього немає посилання в інтерфейсі.
Гідратація - процес, коли React у браузері «підхоплює» HTML, згенерований на сервері: прив'язує обробники подій і стан до вже наявних елементів. React очікує, що перший рендер у браузері дасть точно такий самий результат, що й на сервері. Розбіжність - помилка гідратації.
Наслідки: React виводить помилку й перерендерює частину дерева на клієнті (повільніше, можливе «мигання»), а атрибути можуть лишитися невиправленими. Документація радить вважати розбіжності багами.
Найчастіші причини:
1. Значення, різні на сервері й клієнті:
<p>Згенеровано: {new Date().toLocaleTimeString()}</p> // інший час
<p>{Math.random()}</p>
<p>{price.toLocaleString()}</p> // інша локаль чи часовий пояс
2. Перевірки середовища в рендері:
{typeof window !== 'undefined' && <MobileMenu />} // на сервері немає, на клієнті є
3. Дані з браузера: localStorage, розмір вікна, тема з налаштувань системи - на сервері їх немає.
4. Некоректна вкладеність HTML: <p> всередині <p>, <div> у <p>, <a> у <a> - браузер «виправляє» розмітку при розборі, і дерево відрізняється від очікуваного.
5. Розширення браузера, що змінюють DOM до гідратації (перекладачі, менеджери паролів).
Як виправляти:
- значення лише для клієнта - після монтування:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return <p>Згенеровано: {mounted ? new Date().toLocaleTimeString() : '...'}</p>;
Перший рендер збігається з сервером, а одразу після гідратації - другий з клієнтськими даними. У Next.js - також dynamic(() => import(...), { ssr: false }) для компонентів, яким сервер узагалі не потрібен;
- дати й числа - форматувати з явною локаллю й часовим поясом однаково на обох боках (
Intl.DateTimeFormat('uk', { timeZone: 'Europe/Kyiv' })) або передавати вже відформатований рядок із сервера; - ідентифікатори -
useId, а не лічильник чиMath.random(); - неминуча розбіжність одного елемента (позначка часу) -
suppressHydrationWarningна ньому. Діє лише на один рівень і не виправляє вміст - це запасний вихід, а не рішення; - тема без мигання - встановлювати клас на
<html>вбудованим скриптом до гідратації іsuppressHydrationWarningна<html>.
Діагностика: у режимі розробки React показує, у якому елементі й чим відрізнялися дерева; onRecoverableError у hydrateRoot дає змогу збирати ці помилки в моніторинг на продакшені.
proxy.ts (до Next.js 16 - middleware.ts) - код, що виконується до обробки запиту маршрутом: може переписати URL, перенаправити, змінити заголовки чи cookies або відповісти одразу.
// proxy.ts у корені проєкту
import { NextResponse, type NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
if (!request.cookies.has('session')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
Чому перейменували. Назва «middleware» асоціювалася з middleware Express чи Laravel - ланцюжком обробників навколо запиту, на який кладуть усе підряд. Next.js прямо радить не покладатися на цей механізм, якщо є інші варіанти, і перейменував його на «proxy», щоб підкреслити роль - мережевий рубіж перед застосунком (може навіть виконуватися на CDN, окремо від рендеру). Перехід - кодмод middleware-to-proxy; у Next.js 16 proxy за замовчуванням працює на Node.js runtime.
Для чого proxy доречний:
- швидкі перенаправлення й переписування (локалізація за мовою, A/B-тести, старі URL);
- оптимістичні перевірки: чи є взагалі cookie сесії - перенаправити на вхід, не рендерячи сторінку;
- заголовки (безпека, CORS для обробників маршрутів), логування.
Чому не будувати на ньому авторизацію:
- proxy не замінює перевірок у даних. Сторінка, серверна дія чи обробник маршруту можуть бути викликані способами, які
matcherне охоплює (зміна шляху, нові маршрути, прямий виклик серверної дії). Перевірка має бути там, де читаються й змінюються дані; - у 2025 році була вразливість (CVE-2025-29927), що дозволяла обійти middleware спеціальним заголовком у деяких самостійно розгорнутих версіях. Застосунки, де авторизація була лише в middleware, виявилися відкритими. Багаторівневий захист таку помилку пережив би;
- обмежений доступ до контексту: proxy виконується окремо від рендеру, не повинен покладатися на спільні модулі чи глобальний стан; повна перевірка прав з запитами до бази тут дорога.
Рекомендований підхід:
- proxy - оптимістичне перенаправлення неавтентифікованих користувачів (UX і зменшення навантаження);
- шар доступу до даних з перевіркою сесії й прав - у кожному серверному компоненті, дії, обробнику маршруту.
Аналог у Laravel - різниця між глобальним middleware і authorize() у контролері чи політиках: перший фільтрує, другий справді захищає ресурс.
За замовчуванням кожен перехід в Inertia - це запит до контролера Laravel, який обчислює всі props сторінки. Для сторінки зі списком і фільтрами зміна одного фільтра означала б заново обчислити й довідники, і статистику, і все інше.
Часткове перезавантаження - запит лише потрібних props для поточної сторінки:
import { router } from '@inertiajs/react';
function CompanyFilter({ companies }) {
return (
<select
onChange={(e) =>
router.reload({
data: { company: e.target.value },
only: ['users'], // повернути лише список користувачів
})
}
>
{companies.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
);
}
only: [...]- які props повернути;except: [...]- які пропустити;router.reload()- скорочення для візиту на поточну URL; часткові перезавантаження працюють лише для тієї самої сторінки (того самого компонента).
Чому замикання на сервері обов'язкові. Сервер повертає лише запитані props, але щоб їх не обчислювати, значення мають бути ледачими:
return Inertia::render('Users/Index', [
'users' => fn () => User::query()->filter(request()->only('company'))->paginate(20),
'companies' => fn () => Company::orderBy('name')->get(['id', 'name']),
'stats' => fn () => $statsService->heavyCalculation(),
]);
Без fn () => запит до companies і важкий stats виконуються на кожному запиті, навіть якщо їх потім викинуть з відповіді.
Інші типи props Inertia (v3):
Inertia::optional(fn () => ...)- не надсилається при звичайних переходах, лише коли явно запитаний черезonly(ранішеInertia::lazy, видалено у v3);Inertia::defer(fn () => ...)- сторінка рендериться без цього prop, а він довантажується окремим запитом одразу після показу (повільна статистика);Inertia::merge(...)- нові дані дописуються до наявних, а не замінюють їх (нескінченний скрол, «Завантажити ще»);Inertia::once(...)- обчислюється один раз і запам'ятовується клієнтом між переходами.
Пастки:
- назва prop в
onlyмає точно збігатися з ключем у контролері - помилка в назві мовчки дає «нічого не оновилося»; - спільні дані (з
HandleInertiaRequests) теж підпадають підonly/except- для них правило замикань те саме; - стан сторінки: часткове перезавантаження за замовчуванням зберігає стан компонента (
preserveState) - фільтри й введені значення не скидаються.
Докладніше в документації: Inertia: часткові перезавантаження