React і Next.js
Як React перетворює стан на інтерфейс, де потрібні ефекти й де зберігати дані, як Next.js рендерить і кешує сторінки на сервері та що змінили React 19 і Server Components. Версії — на жовтень 2026: React 19.3, Next.js 16.4.
Перед цією темою: JavaScript і TypeScript.
Як влаштований React: компоненти, props, state і хуки?
Section titled “Як влаштований React: компоненти, props, state і хуки?”Що це
- Компонент — функція, яка повертає JSX: розмітку, схожу на HTML, просто в JavaScript.
- Props — вхідні параметри від батька, лише для читання.
- State — дані, які компонент пам’ятає між рендерами.
Як користуватися
const [count, setCount] = useState(0)— значення й функція його зміни.- Виклик
setCount(count + 1)запускає новий рендер з новим значенням. - Список рендерять через
map, кожному елементу даютьkey— стабільний id. - Поле введення роблять контрольованим:
valueбереться зі state,onChangeзмінює state.
Типова помилка
Змінювати state напряму: todos.push(x). Посилання на масив те саме, і React не побачить змін. Потрібен новий масив: setTodos([...todos, x]).
Як влаштовано
- Рендер. React викликає функцію компонента й отримує дерево елементів.
- Commit. Порівнює його з попереднім деревом і точково змінює DOM.
- Повтор. Новий state або рендер батька знову запускає компонент і за замовчуванням усіх його дітей.
Правила хуків
- Лише на верхньому рівні компонента або свого хука: не в умовах, циклах і після раннього
return. React знаходить стан хуків за порядком виклику. - Лише з компонентів і хуків, чиє ім’я починається з
use. - Порушення ловить
eslint-plugin-react-hooks.
Пастки
- State — знімок. Після
setCount(count + 1)зміннаcountдо наступного рендера та сама. Три такі виклики поспіль дадуть +1, аsetCount(c => c + 1)— +3. - Індекс як
key. Під час вставки на початок або сортування введення й фокус рядків переїдуть до чужих елементів. - Односпрямований потік. Дитина не змінює props, а викликає колбек батька:
onChange,onSelect.
Правило
Зберігайте в state мінімум, решту обчислюйте в рендері. Спільний стан кладіть у найближчого спільного батька.
Під капотом
- Кожному екземпляру компонента відповідає вузол fiber. У ньому лежить список хуків у порядку виклику, тому умовний хук зсуває список і ламає стан.
- Узгодження евристичне й лінійне: елемент іншого типу не порівнюється вглиб, а створюється заново разом з піддеревом. Діти списку зіставляються за
key. - З React 18 оновлення пакетуються всюди (automatic batching): кілька
setStateв обробнику, промісі чи таймері дають один рендер.
Ціна
- Рендер дешевший за commit, але не безкоштовний: перемальовування великого піддерева на кожне натискання клавіші помітне. Стан, що часто змінюється, тримають у листових компонентах.
- Зміна
keyабо типу елемента скидає стан усього піддерева. Це й інструмент:key={userId}скидає форму під час зміни користувача.
Компроміси
- Контрольоване поле дає валідацію на кожне введення ціною рендера на кожен символ. Неконтрольовані поля з
FormDataдешевші, якщо значення потрібне лише під час надсилання. - StrictMode у розробці викликає компонент двічі, щоб спіймати побічні ефекти в рендері. На тій самій чистоті побудовані конкурентний рендеринг і React Compiler.
Як перевірити в продакшені
- Profiler у React DevTools з опцією «Record why each component rendered while profiling» показує причину кожного рендера.
- Опція «Highlight updates when components render» підсвічує перемальовані компоненти.
Підсумок: ідентичність компонента — його позиція та key, а чистий рендер — умова, на якій тримаються всі оптимізації React.
import { useState } from 'react';
type Todo = { id: number; title: string };
export function TodoList({ initial }: { initial: Todo[] }) { const [todos, setTodos] = useState(initial); // стан компонента const [title, setTitle] = useState(''); // контрольоване поле
function add() { // Новий масив, а не push: React порівнює посилання setTodos([...todos, { id: Date.now(), title }]); setTitle(''); }
return ( <> <input value={title} onChange={(e) => setTitle(e.target.value)} /> <button onClick={add} disabled={!title}>Add</button> <ul> {todos.map((t) => <li key={t.id}>{t.title}</li>)} {/* key — стабільний id, не індекс */} </ul> </> );}- Чому не можна викликати хук усередині
if? — React знаходить стан хуків за порядком виклику, умова зсуває порядок. - Що дасть
setCount(count + 1)тричі поспіль? — +1: усі виклики бачать один знімокcount; для накопичення потрібна функціяc => c + 1. - Чому індекс — поганий
key? — Під час вставки й сортування стан рядків дістається чужим елементам. - Як скинути стан компонента під час зміни користувача? — Передати
key={userId}: новий ключ — новий екземпляр. - Чи перемалюється дитина, якщо її props не змінилися? — Так, за замовчуванням рендер батька рендерить дітей; це змінюють
memoабо React Compiler.
Як правильно використовувати useEffect і де він не потрібен?
Section titled “Як правильно використовувати useEffect і де він не потрібен?”Що це
useEffect(setup, deps) виконує setup після того, як React оновив екран. Функція, яку повернув setup, — очищення.
Як користуватися
- Усередині — робота із зовнішнім світом:
addEventListener,setInterval, WebSocket. - У масив залежностей — усе, що ефект читає з props і state.
- Поверніть очищення:
removeEventListener,clearInterval, закриття з’єднання.
| Залежності | Коли запускається |
|---|---|
| масиву немає | після кожного рендера |
[] |
один раз після появи на екрані |
[userId] |
під час появи та зміни userId |
Типова помилка
Синхронізувати ефектом похідні дані: useEffect(() => setFullName(first + ' ' + last), [first, last]). Це зайвий рендер; досить const fullName = first + ' ' + last просто в компоненті.
Як влаштовано
- Рендер → commit у DOM → браузер малює → React запускає ефекти.
- На наступному рендері React порівнює залежності через
Object.is. Якщо щось змінилося — cleanup попереднього ефекту, потім новий setup. - Під час демонтування виконується останній cleanup.
Пастки
- Застаріле замикання. Ефект бачить props і state свого рендера.
setInterval(() => setCount(count + 1))з[]застрягне на 1; потрібноsetCount(c => c + 1). - Об’єкт або функція в залежностях. Вони створюються заново в кожному рендері, і ефект перезапускається завжди. Створіть їх усередині ефекту або залежте від примітивів.
- Гонка відповідей. Під час швидкої зміни
queryстара відповідь може прийти останньою. ПрапорецьignoreабоAbortControllerу cleanup відкидає її. - Подвійний запуск у розробці. Якщо через нього ламається логіка, в ефекту немає коректного очищення.
Коли ефект не потрібен
| Задача | Замість ефекту |
|---|---|
| похідне значення | обчислити в рендері, дороге — useMemo |
| реакція на клік або надсилання | обробник події |
| скидання state під час зміни prop | key у компонента |
| завантаження даних | TanStack Query, серверний компонент |
Правило
Ефект потрібен, коли код виконується тому, що компонент показано, а не тому, що користувач щось зробив.
Під капотом
useEffectвиконується після відмальовування й не блокує його.useLayoutEffectвиконується до відмальовування, для вимірювання DOM, і затримує кадр.- Під час серверного рендерингу ефекти не виконуються — лише в браузері.
- StrictMode у розробці додає кожному ефекту цикл setup → cleanup → setup. У продакшені його немає, але він моделює реальні сценарії: перехід зі сторінки й повернення, Fast Refresh.
Ціна
Завантаження даних в ефекті стартує лише після завантаження JS і рендера. Вкладені компоненти з власними ефектами утворюють водоспад: запит дитини чекає відповіді батька. Кешу, дедуплікації та повторів у разі помилки немає.
Компроміси
- Прочитати свіже значення без перезапуску, наприклад тему всередині обробника з’єднання, —
useEffectEvent(стабільний з React 19.2). Вимикати правилоexhaustive-deps— ховати баг. - Підписка на зовнішнє сховище —
useSyncExternalStore, а не ефект ізsetState: він коректний під час конкурентного рендерингу. - Дані — серверний компонент або TanStack Query: кеш, скасування й гонки розв’язані там.
Як перевірити в продакшені
eslint-plugin-react-hooksзexhaustive-depsіset-state-in-effect— помилкою в CI.- У Profiler React DevTools ланцюжок «ефект →
setState→ рендер» видно як зайві коміти поспіль.
Підсумок: ефект — інструмент синхронізації із зовнішнім світом; що менше ефектів, то менше водоспадів, гонок і зайвих рендерів.
import { useEffect, useState } from 'react';
type Item = { name: string; active: boolean };
export function Search({ query, onlyActive }: { query: string; onlyActive: boolean }) { const [items, setItems] = useState<Item[]>([]);
// Ефект: синхронізація із зовнішньою системою (сервером) під час зміни query useEffect(() => { let ignore = false; // відповідь на застарілий query не перезапише нову fetch(`/api/items?q=${encodeURIComponent(query)}`) .then((r) => r.json()) .then((data: Item[]) => { if (!ignore) setItems(data); }); return () => { ignore = true; }; // очищення: перед перезапуском і під час демонтування }, [query]);
// Не ефект: похідне значення рахується просто в рендері const visible = onlyActive ? items.filter((i) => i.active) : items;
return <ul>{visible.map((i) => <li key={i.name}>{i.name}</li>)}</ul>;}import { useEffect, useState } from 'react';
export function Timer() { const [count, setCount] = useState(0);
useEffect(() => { // Помилка: setCount(count + 1) бачить count = 0 з першого рендера — застрягне на 1 const id = setInterval(() => setCount((c) => c + 1), 1000); // апдейтер бере актуальне значення return () => clearInterval(id); }, []);
return <p>{count}</p>; // 1, 2, 3...}- Чим
useEffectвідрізняється відuseLayoutEffect? — Layout-ефект виконується до відмальовування, для вимірювання DOM; звичайний — після. - Чому ефект у розробці спрацьовує двічі? — StrictMode перевіряє, що очищення скасовує setup; у продакшені повторного запуску немає.
- Як не перезапускати ефект, якщо потрібно лише прочитати свіже значення? — Винести читання в
useEffectEvent(React 19.2+). - Як скасувати запит під час зміни параметрів? —
AbortControllerіabort()у cleanup або прапорецьignore. - Чи можна зробити функцію ефекту
async? — Ні: вона має повернути cleanup, а не проміс; async-функцію оголошують усередині й викликають.
Як керувати станом у React-застосунку?
Section titled “Як керувати станом у React-застосунку?”Що це
Стан — дані, від яких залежить екран: чи відкритий діалог, текст у полі, список замовлень.
Як користуватися
- Почніть з
useStateу компоненті, якому він потрібен. - Потрібен двом компонентам — підніміть state у найближчого спільного батька й передайте вниз значення та функцію зміни.
- Значення потрібне глибоко й у багатьох місцях (тема, поточний користувач) — Context.
- Дані із сервера —
useQueryз TanStack Query: він завантажить, закешує й оновить їх.
Типова помилка
Копіювати props у state: useState(props.user.name). Початкове значення береться один раз, і під час зміни props state залишиться старим.
Як влаштовано
- Підняття стану. State переїжджає в спільного батька, діти отримують значення й колбек. Ціна — props проходять через компоненти, яким вони не потрібні.
- Context.
createContext, провайдер<ThemeContext value={theme}>(React 19) іuseContextабоuse. Під час зміни значення перемальовуються всі споживачі. - Стор. Zustand і Redux Toolkit зберігають стан поза деревом React. Компонент підписується селектором і перемальовується, лише коли змінюється вибрана частина.
- Серверний стан. TanStack Query зберігає відповіді в кеші за
queryKey. Після мутаціїinvalidateQueriesпозначає дані застарілими й перезапитує активні.
Коли що обирати
| Стан | Де зберігати |
|---|---|
| діалог відкритий, введення в полі | useState у компоненті |
| потрібен сусідам | спільний батько |
| тема, мова, користувач | Context |
| кошик, складний спільний UI | Zustand або Redux Toolkit |
| дані з API | TanStack Query, серверний компонент |
| фільтри, сторінка, вкладка | URL (searchParams) |
Пастки
- Новий об’єкт у
valueпровайдера на кожному рендері перемальовує всіх споживачів. Розділяйте контексти або мемоізуйтеvalue. - Селектор Zustand, що повертає новий об’єкт або масив, перемальовує на кожну зміну стору — обгорніть його в
useShallow.
Правило
Спершу вирішіть, чиї це дані: сервера, URL чи інтерфейсу. Від цього залежить місце зберігання.
Під капотом
- Context порівнює
valueчерезObject.isі перемальовує всіх споживачів нижче провайдера;memoїх не зупиняє. - Zustand побудований на
useSyncExternalStore: компонент перемальовується, лише якщо результат селектора змінився заObject.is. - TanStack Query (v5) за замовчуванням вважає дані одразу застарілими (
staleTime: 0) і перезапитує їх під час монтування, фокусу вікна та відновлення мережі. Невикористаний кеш живе 5 хвилин (gcTime).
Ціна
- Без
staleTimeкожне монтування й повернення на вкладку — мережевий запит. Для довідників ставлять хвилини. queryKey— контракт: неточний ключ дає застарілий екран після мутації або зайві запити.
Компроміси
- Zustand — мінімум коду й хук без провайдера. Redux Toolkit — сувора структура, історія дій у DevTools і RTK Query; виправданий у великій команді.
- У Next.js App Router дані читають у серверних компонентах. TanStack Query залишається для інтерактивного клієнта: нескінченні списки, опитування, оптимістичні оновлення.
- Стор на рівні модуля на сервері Next.js спільний для всіх запитів, і дані одного користувача витечуть до іншого. Документація Zustand радить створювати стор на запит і передавати через Context.
Як перевірити в продакшені
- TanStack Query Devtools показує ключі, свіжість і кількість підписників запиту.
- Profiler React DevTools показує, скільки компонентів перемалювала зміна значення Context.
Підсумок: вибір сховища — вибір власника даних, а швидкість залежить від гранулярності підписок.
import { create } from 'zustand';import { useShallow } from 'zustand/react/shallow';
type CartState = { items: { id: number; qty: number }[]; add: (id: number) => void;};
export const useCart = create<CartState>()((set) => ({ items: [], add: (id) => set((s) => ({ items: [...s.items, { id, qty: 1 }] })), // новий масив}));
export function CartBadge() { // Селектор: перемальовування лише під час зміни кількості товарів const count = useCart((s) => s.items.length); return <span>{count}</span>;}
export function CartIds() { // map створює новий масив на кожен виклик — useShallow порівнює поелементно const ids = useCart(useShallow((s) => s.items.map((i) => i.id))); return <p>{ids.join(', ')}</p>;}import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
type Order = { id: number; status: string };
export function Orders() { const queryClient = useQueryClient();
const { data, isPending } = useQuery({ queryKey: ['orders'], // ключ кешу queryFn: (): Promise<Order[]> => fetch('/api/orders').then((r) => r.json()), staleTime: 30_000, // 30 секунд дані свіжі: без повторних запитів });
const close = useMutation({ mutationFn: (id: number) => fetch(`/api/orders/${id}/close`, { method: 'POST' }), // Інвалідація: кеш застарів, активні запити перезапитаються onSuccess: () => queryClient.invalidateQueries({ queryKey: ['orders'] }), });
if (isPending) return <p>Loading…</p>; return ( <ul> {data?.map((o) => ( <li key={o.id}> #{o.id} {o.status} <button onClick={() => close.mutate(o.id)}>Close</button> </li> ))} </ul> );}- Чому перемальовуються всі споживачі Context? — Підписка на весь
value: під час зміни посилання оновлюються всі,memoне допомагає. - Чим серверний стан відрізняється від клієнтського? — Власник — сервер: дані застарівають, їх кешують, перезапитують та інвалідують.
- Що таке
staleTimeіgcTime? — Скільки дані вважаються свіжими й скільки невикористаний кеш живе в пам’яті; за замовчуванням 0 і 5 хвилин. - Коли Redux Toolkit, а не Zustand? — Велика команда, суворі домовленості, історія дій у DevTools, RTK Query.
- Чому глобальний стор небезпечний у Next.js? — Модуль на сервері спільний для всіх запитів; стор створюють на запит.
Як влаштований Next.js і App Router?
Section titled “Як влаштований Next.js і App Router?”Що це
Next.js додає до React маршрутизацію, рендеринг на сервері, збирання й серверний код. App Router — маршрутизатор на основі папки app/.
Як користуватися
app/page.tsx— це/,app/blog/[slug]/page.tsx—/blog/hello. Квадратні дужки — динамічний сегмент.layout.tsxобгортає всі сторінки під собою: шапка, меню.loading.tsxіerror.tsx— екрани завантаження й помилки для сегмента.- Серверний компонент може бути
asyncі отримувати дані черезawait. - Потрібні
useStateабоonClick— перший рядок файлу'use client'.
Типова помилка
Ставити 'use client' на всю сторінку заради однієї кнопки. Тоді весь код сторінки йде в браузер; у клієнтський компонент виносять лише кнопку.
Як влаштовано
- Запит проходить
proxy.ts(до Next.js 16 —middleware.ts), якщо він є: редиректи, перезапис URL, заголовки. - Серверні компоненти рендеряться в RSC Payload — серіалізоване дерево з місцями для клієнтських компонентів.
- З нього будується HTML першого завантаження, а браузер завантажує JS клієнтських компонентів.
- Під час переходу за
<Link>сервер віддає лише сегменти, що змінилися; спільні layout не перемальовуються.
Коли що обирати
| Режим | Коли готовий HTML |
|---|---|
| SSG | під час збирання; однаковий для всіх |
| ISR | під час збирання, оновлюється у фоні за часом або тегом |
| SSR | на кожен запит; дані користувача |
Серверний код
- Server Actions (
'use server') — функції для зміни даних з форм і обробників. Викликаються POST-запитом. - Route Handlers (
app/api/.../route.tsзexport async function GET) — HTTP API для вебхуків і зовнішніх клієнтів, наприклад мобільного застосунку.
Пастки
- З Next.js 15
params,searchParams,cookies()іheaders()асинхронні, у 16 синхронний доступ вилучено. - З Next.js 15
fetchі GET Route Handlers за замовчуванням не кешуються. - Server Action — публічний ендпоінт: авторизацію перевіряють усередині кожного.
Правило
Читають дані в серверних компонентах, змінюють — у Server Actions, Route Handler — для зовнішніх споживачів.
Під капотом
- З
cacheComponents: trueусе динамічне виконується на запит.'use cache'у функції, компонента або файлу кешує результат; ключ — аргументи й захоплені значення, тому вони серіалізовні. - Під час збирання статичне й закешоване потрапляє в статичну оболонку, а замість даних запиту під
Suspense— fallback. Оболонку роздає CDN, решта стрімиться в тій самій відповіді. cookies()іheaders()усередині'use cache'недоступні: значення читають ззовні й передають аргументом або використовують'use cache: private'.
Інвалідація
| API | Семантика |
|---|---|
updateTag(tag) |
лише в Server Action; автор одразу бачить свої зміни |
revalidateTag(tag, 'max') |
stale-while-revalidate: віддається старе, нове будується у фоні |
refresh() |
оновити некешовані дані, кеш не чіпає |
Компроміси
- Без Cache Components діє попередня модель: маршрут статичний, поки не читає даних запиту, а
export const revalidateзадає ISR. Одне читанняcookies()робить динамічною всю сторінку. - За замовчуванням кеш
'use cache'живе в пам’яті екземпляра. Спільний для кількох екземплярів —'use cache: remote'з обробником кешу. proxy.tsпрацює на Node.js;middleware.tsдля Edge застарів. Proxy — для швидких редиректів і заголовків, не для повної авторизації.- Розгортання: Vercel,
next startабоoutput: 'standalone'у Docker, адаптери платформ (стабільні з 16.2).output: 'export'— статика без серверних можливостей.
Як перевірити в продакшені
next buildдрукує тип кожного маршруту, dev-оверлей показує звернення до даних запиту позаSuspense.export const ensureStatic = 'navigation'(16.4) валить збирання, якщо в статичний маршрут потрапив динамічний компонент.
Підсумок: у Next.js 16 кеш явний і гранулярний: статична оболонка з CDN, динаміка стрімиться, інвалідація — за тегами.
// app/products/[id]/page.tsx — серверний компонент за замовчуваннямimport { Suspense } from 'react';import { cacheLife, cacheTag } from 'next/cache';import { AddToCart } from './add-to-cart'; // файл з 'use client'
type Props = { params: Promise<{ id: string }> }; // з Next.js 15 params — проміс
export default function Page({ params }: Props) { return ( <Suspense fallback={<p>Loading…</p>}> {/* fallback потрапляє в статичну оболонку */} <Product params={params} /> </Suspense> );}
async function Product({ params }: Props) { const { id } = await params; const product = await getProduct(id); return ( <main> <h1>{product.name}</h1> <AddToCart id={id} /> {/* у браузер іде JS лише цього компонента */} </main> );}
async function getProduct(id: string) { 'use cache'; // Cache Components: результат кешується за аргументами cacheLife('hours'); cacheTag(`product-${id}`); // скинути після зміни: updateTag у Server Action const res = await fetch(`https://api.example.com/products/${id}`); return (await res.json()) as { name: string };}'use server';import { updateTag } from 'next/cache';
export async function renameProduct(id: string, formData: FormData) { const session = await auth(); // публічний POST-ендпоінт: перевіряємо права тут if (!session?.isAdmin) throw new Error('Forbidden'); await db.products.update(id, { name: String(formData.get('name')) }); updateTag(`product-${id}`); // автор одразу побачить нову назву}
// app/api/products/route.ts — HTTP API для зовнішніх клієнтівexport async function GET() { return Response.json(await db.products.list());}- Чим серверний компонент відрізняється від клієнтського? — Код серверного не потрапляє в браузер, він може бути
async, але без state і подій. - Чи можна вкласти серверний компонент у клієнтський? — Імпорт зробить його клієнтським; серверний передають як
childrenабо інший prop. - Як перейменували middleware в Next.js 16? — На
proxy.tsз функцієюproxy, на Node.js;middleware.tsдля Edge застарів. - Чим
updateTagвідрізняється відrevalidateTag? —updateTagу Server Action одразу показує свіжі дані,revalidateTagвіддає старі й оновлює у фоні. - Коли Route Handler, а коли Server Action? — Action — мутації зі свого UI, Route Handler — HTTP API для чужих клієнтів і вебхуків.
Що нового в React 19 і як досягти швидкого рендерингу?
Section titled “Що нового в React 19 і як досягти швидкого рендерингу?”Що це
- Action — функція, передана в
<form action={...}>: отримуєFormData, а React сам знає, чи триває надсилання. useActionState— результат останньої дії та прапорецьisPending.useOptimistic— тимчасове значення, яке видно, поки дія виконується.- React Compiler — плагін збирання, який сам кешує обчислення й пропускає зайві рендери.
Як користуватися
const [error, formAction, isPending] = useActionState(save, null).<form action={formAction}>і кнопка зdisabled={isPending}.- Компілятор у Next.js 16 вмикають опцією
reactCompiler: true; в Expo SDK 54+ він увімкнений за замовчуванням.
Типова помилка
Обгортати кожну функцію в useCallback «для швидкості». Без вимірювань це шум, а з компілятором — зайвий код.
Як влаштовано
- Перехід.
startTransitionпозначає оновлення нетерміновим: введення лишається чутливим, а старий UI видно, поки готується новий. Action — асинхронна функція всередині переходу. useOptimistic. Сетер викликають усередині Action. Коли дія завершилася, оптимістичне значення змінюється реальним, у разі помилки — зникає.use. Читає проміс, призупиняючи компонент до результату, або контекст. Його, на відміну від інших хуків, можна викликати в умовах.- Suspense і стрімінг. Найближчий
Suspenseпоказує fallback, поки компонент чекає дані. Сервер одразу надсилає оболонку, а готові частини дописує в ту саму відповідь. - Гідратація. React прив’язує обробники до HTML, що прийшов із сервера. Межі
Suspenseгідратуються незалежно, першою — та, з якою взаємодіє користувач.
Пастки
- Помилка гідратації — HTML сервера не збігся з першим клієнтським рендером:
Date.now(),typeof window, локаль,<div>усередині<p>. React перемалює ділянку на клієнті — це повільніше. - Проміс, створений у рендері клієнтського компонента, для
useстворюється заново на кожен рендер. Його передають із серверного компонента або беруть із кешувальної бібліотеки.
Правило
Спершу виміряйте в Profiler, потім оптимізуйте: опустіть стан нижче, розбийте сторінку на Suspense, увімкніть компілятор.
Під капотом
- Серверні компоненти виконуються на сервері й серіалізуються в RSC Payload; їхній код і залежності не потрапляють у бандл.
- Межа
'use client'— межа серіалізації. Props крізь неї: примітиви, прості об’єкти, масиви,Date,Map,Set, проміси, JSX і Server Functions. Звичайні функції та екземпляри класів передати не можна. - React Compiler 1.0 (жовтень 2025) під час збирання аналізує потік даних і мемоізує значення, JSX і колбеки, зокрема після раннього
return, деuseMemoнеможливий. Компонент, що порушує правила React, він пропускає.
Компроміси
useMemoіuseCallbackлишаються для точного контролю, наприклад стабільної залежності ефекту. Наявну мемоізацію прибирають лише з тестами: це змінює результат компіляції.- У Next.js 16 компілятор не ввімкнений за замовчуванням і працює через Babel, збирання повільніше; Rust-версія експериментальна з 16.3.
- Стрімінг пришвидшує перший байт, але кожна межа
Suspense— окремий стан завантаження. Надто дрібні межі дають розсип спінерів. - Компонент лише для браузера (
localStorage, часовий пояс) —use(browser())з React 19.3 замість прапорцяmountedв ефекті.
Як перевірити в продакшені
- Profiler у React DevTools: flamegraph комітів і причини рендерів. Компоненти, оптимізовані компілятором, позначені бейджем «Memo ✨».
- Performance Tracks у Chrome DevTools (React 19.2) показують роботу планувальника й компонентів на одній шкалі з мережею.
- Помилки гідратації видно в консолі з діфом сервер/клієнт, а в продакшені — через
onRecoverableError.
Підсумок: швидкий React — менше JS на клієнті завдяки серверним компонентам, стрімінг, розбитий межами Suspense, і автоматична мемоізація замість ручної.
'use client';import { useActionState, useOptimistic } from 'react';import { addComment } from './actions'; // Server Function: (prev, formData) => Promise<string | null>
type Comment = { id: string; text: string };
export function Comments({ comments }: { comments: Comment[] }) { const [optimistic, addOptimistic] = useOptimistic( comments, (list, c: Comment) => [...list, c], ); const [error, formAction, isPending] = useActionState( async (prev: string | null, formData: FormData) => { // Видно одразу, до відповіді сервера; у разі помилки рядок зникне addOptimistic({ id: crypto.randomUUID(), text: String(formData.get('text')) }); return addComment(prev, formData); // текст помилки або null }, null, );
return ( <form action={formAction}> <ul>{optimistic.map((c) => <li key={c.id}>{c.text}</li>)}</ul> <input name="text" /> <button disabled={isPending}>Send</button> {error && <p role="alert">{error}</p>} </form> );}// app/orders/page.tsx — серверний компонентimport { Suspense } from 'react';import { OrderList } from './order-list';
export default function Page() { const orders = getOrders(); // без await: запит стартує одразу return ( <Suspense fallback={<p>Loading…</p>}> {/* fallback іде в HTML першим */} <OrderList orders={orders} /> {/* проміс серіалізовний — його можна передати */} </Suspense> );}
// app/orders/order-list.tsx'use client';import { use } from 'react';
export function OrderList({ orders }: { orders: Promise<{ id: number }[]> }) { const list = use(orders); // призупиняє компонент до відповіді return <ul>{list.map((o) => <li key={o.id}>#{o.id}</li>)}</ul>;}- Що можна передати із серверного компонента в клієнтський? — Серіалізовне: примітиви, прості об’єкти, масиви,
Date,Map,Set, проміси, JSX, Server Functions; не звичайні функції й класи. - Чим
useActionStateвідрізняється відuseFormStatus? — Перший зберігає результат і очікування конкретної дії;useFormStatusзreact-domчитає статус батьківської форми в дочірньому компоненті. - Чи потрібні
useMemoіuseCallbackз React Compiler? — Рідко: для точного контролю, наприклад стабільної залежності ефекту; стару мемоізацію прибирають лише з тестами. - Як виправити помилку гідратації? — Прибрати відмінність сервер/клієнт: браузерне — в ефект або
use(browser());suppressHydrationWarning— лише для тексту на кшталт дати. - Чим
useTransitionвідрізняється відuseDeferredValue? — Перший позначає нетерміновим своє оновлення, другий відкладає значення, оновлення якого ви не контролюєте.