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

Питання на співбесіді: 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.

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

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 не потрібен.

Докладніше в документації: 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: макети й сторінки

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, з тими самими правилами: усе з префіксом публічне й фіксується при збиранні.

Докладніше в документації: Next.js: змінні оточення

У 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 з тегами, але вбудований у рендер сторінок.

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

Деякі дані потрібні на кожній сторінці: поточний користувач для шапки, назва застосунку, флеш-повідомлення, налаштування. Передавати їх у кожному контролері незручно - для цього є спільні дані (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() - їх не потрібно вручну додавати в спільні дані.

Докладніше в документації: Inertia: спільні дані

У режимі даних маршрут описується об'єктом, до якого прив'язані завантажувач (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, куди вона звертається.

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

Серверна функція з '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() лише тому, що на нього немає посилання в інтерфейсі.

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

Гідратація - процес, коли 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 дає змогу збирати ці помилки в моніторинг на продакшені.

Докладніше в документації: 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 виконується окремо від рендеру, не повинен покладатися на спільні модулі чи глобальний стан; повна перевірка прав з запитами до бази тут дорога.

Рекомендований підхід:

  1. proxy - оптимістичне перенаправлення неавтентифікованих користувачів (UX і зменшення навантаження);
  2. шар доступу до даних з перевіркою сесії й прав - у кожному серверному компоненті, дії, обробнику маршруту.

Аналог у Laravel - різниця між глобальним middleware і authorize() у контролері чи політиках: перший фільтрує, другий справді захищає ресурс.

Докладніше в документації: Next.js: proxy.js

За замовчуванням кожен перехід в 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: часткові перезавантаження