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