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

Middle: питання на співбесіді з теми «Next.js, Inertia й маршрутизація»

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

4 питання

У 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: дії