Перейти к содержимому

React и Next.js

Как React превращает состояние в интерфейс, где нужны эффекты и где хранить данные, как Next.js рендерит и кеширует страницы на сервере и что изменили React 19 и Server Components. Версии — на октябрь 2026: React 19.3, Next.js 16.4.

Перед этой темой: JavaScript и TypeScript.

Junior+

Как устроен React: компоненты, props, state и хуки?

Заголовок раздела «Как устроен 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 и где он не нужен?

Заголовок раздела «Как правильно использовать 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-приложении?

Заголовок раздела «Как управлять состоянием в 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 добавляет к 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 и как добиться быстрого рендеринга?

Заголовок раздела «Что нового в 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>;
}