React и Next.js
Как React превращает состояние в интерфейс, где нужны эффекты и где хранить данные, как Next.js рендерит и кеширует страницы на сервере и что изменили React 19 и Server Components. Версии — на октябрь 2026: React 19.3, Next.js 16.4.
Перед этой темой: JavaScript и TypeScript.
Как устроен React: компоненты, props, state и хуки?
Заголовок раздела «Как устроен 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 и где он не нужен?
Заголовок раздела «Как правильно использовать 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-приложении?
Заголовок раздела «Как управлять состоянием в 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?
Заголовок раздела «Как устроен 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 и как добиться быстрого рендеринга?
Заголовок раздела «Что нового в 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? — Первый помечает несрочным своё обновление, второй откладывает значение, обновление которого вы не контролируете.