Перейти до вмісту

React і Next.js

Як React перетворює стан на інтерфейс, де потрібні ефекти й де зберігати дані, як Next.js рендерить і кешує сторінки на сервері та що змінили React 19 і Server Components. Версії — на жовтень 2026: React 19.3, Next.js 16.4.

Перед цією темою: JavaScript і TypeScript.

Junior+

Як влаштований React: компоненти, props, state і хуки?

Section titled “Як влаштований React: компоненти, props, state і хуки?”
Відповідь

Що це

  • Компонент — функція, яка повертає JSX: розмітку, схожу на HTML, просто в JavaScript.
  • Props — вхідні параметри від батька, лише для читання.
  • State — дані, які компонент пам’ятає між рендерами.

Як користуватися

  1. const [count, setCount] = useState(0) — значення й функція його зміни.
  2. Виклик setCount(count + 1) запускає новий рендер з новим значенням.
  3. Список рендерять через map, кожному елементу дають key — стабільний id.
  4. Поле введення роблять контрольованим: value береться зі state, onChange змінює state.

Типова помилка

Змінювати state напряму: todos.push(x). Посилання на масив те саме, і React не побачить змін. Потрібен новий масив: setTodos([...todos, x]).

Як влаштовано

  1. Рендер. React викликає функцію компонента й отримує дерево елементів.
  2. Commit. Порівнює його з попереднім деревом і точково змінює DOM.
  3. Повтор. Новий 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>
</>
);
}
Middle+

Як правильно використовувати useEffect і де він не потрібен?

Section titled “Як правильно використовувати useEffect і де він не потрібен?”
Відповідь

Що це

useEffect(setup, deps) виконує setup після того, як React оновив екран. Функція, яку повернув setup, — очищення.

Як користуватися

  1. Усередині — робота із зовнішнім світом: addEventListener, setInterval, WebSocket.
  2. У масив залежностей — усе, що ефект читає з props і state.
  3. Поверніть очищення: removeEventListener, clearInterval, закриття з’єднання.
Залежності Коли запускається
масиву немає після кожного рендера
[] один раз після появи на екрані
[userId] під час появи та зміни userId

Типова помилка

Синхронізувати ефектом похідні дані: useEffect(() => setFullName(first + ' ' + last), [first, last]). Це зайвий рендер; досить const fullName = first + ' ' + last просто в компоненті.

Як влаштовано

  1. Рендер → commit у DOM → браузер малює → React запускає ефекти.
  2. На наступному рендері React порівнює залежності через Object.is. Якщо щось змінилося — cleanup попереднього ефекту, потім новий setup.
  3. Під час демонтування виконується останній 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...
}
Middle+

Як керувати станом у React-застосунку?

Section titled “Як керувати станом у React-застосунку?”
Відповідь

Що це

Стан — дані, від яких залежить екран: чи відкритий діалог, текст у полі, список замовлень.

Як користуватися

  1. Почніть з useState у компоненті, якому він потрібен.
  2. Потрібен двом компонентам — підніміть state у найближчого спільного батька й передайте вниз значення та функцію зміни.
  3. Значення потрібне глибоко й у багатьох місцях (тема, поточний користувач) — Context.
  4. Дані із сервера — 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.

Підсумок: вибір сховища — вибір власника даних, а швидкість залежить від гранулярності підписок.

Клієнтський стан: Zustand
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>;
}
Серверний стан: TanStack Query
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>
);
}
Middle+

Як влаштований 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' на всю сторінку заради однієї кнопки. Тоді весь код сторінки йде в браузер; у клієнтський компонент виносять лише кнопку.

Як влаштовано

  1. Запит проходить proxy.ts (до Next.js 16 — middleware.ts), якщо він є: редиректи, перезапис URL, заголовки.
  2. Серверні компоненти рендеряться в RSC Payload — серіалізоване дерево з місцями для клієнтських компонентів.
  3. З нього будується HTML першого завантаження, а браузер завантажує JS клієнтських компонентів.
  4. Під час переходу за <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 };
}
Server Action і Route Handler
app/products/[id]/actions.ts
'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());
}
Senior+

Що нового в React 19 і як досягти швидкого рендерингу?

Section titled “Що нового в React 19 і як досягти швидкого рендерингу?”
Відповідь

Що це

  • Action — функція, передана в <form action={...}>: отримує FormData, а React сам знає, чи триває надсилання.
  • useActionState — результат останньої дії та прапорець isPending.
  • useOptimistic — тимчасове значення, яке видно, поки дія виконується.
  • React Compiler — плагін збирання, який сам кешує обчислення й пропускає зайві рендери.

Як користуватися

  1. const [error, formAction, isPending] = useActionState(save, null).
  2. <form action={formAction}> і кнопка з disabled={isPending}.
  3. Компілятор у 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>;
}