Оценка необходимости обновления до React 19: какие новые API заменят существующие решения
Практический обзор использования Server Actions, useOptimistic и React Compiler, включая важные ограничения и план по первому внедрению изменений React 19.
React 19 добавил больше функций API, чем любая предыдущая версия за многие годы, и из-за огромного количества информации трудно понять, что действительно важно для существующей базы кода. Для большинства команд несколько новых функций заменяют существующие решения, одна меняет подход к загрузке данных, а остальное можно отложить. Ниже приведены наиболее важные изменения с примерами кода до и после обновления, а также важные нюансы, которые легко упустить, чтобы вы могли спланировать обновление с учетом его реальной пользы.
use: чтение Promises и Context во время отрисовки
API use является самым значимым нововведением, и его поведение отличается от всех известных вам хуков. Обычные хуки должны вызываться на верхнем уровне компонента в одинаковом порядке при каждой отрисовке. use не подчиняется этому правилу: его можно вызывать после раннего возврата, внутри условия или цикла.
В приведённом ниже примере компонент возвращает вид для гостей, когда отсутствует userId, и только затем загружает данные пользователя:
// React 19 — use() can be called inside conditionals and loops
function UserProfile({ userId }) {
if (!userId) return <GuestView />;
// This is valid in React 19
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
Настоящая мощность заключается в том, что принимает функция use: Promise или Context. При получении Promise React приостанавливает работу компонента до его решения. Приведённое ниже сравнение показывает, насколько много кода исчезает. В старой версии данные и флаг загрузки хранятся в состоянии, запрос выполняется через effect, а спиннер рендерится вручную; в версии React 19 значение читается напрямую:
// Before React 19
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
// React 19
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
Работа не исчезает, а просто перемещается: ближайший контейнер <Suspense> отображает запасной вариант, пока Promise находится в состоянии ожидания, а ближайший контейнер обработки ошибок справляется с их возникновением. Компонент описывает только путь успешного выполнения.
Promise должен быть стабильным
use ожидает один и тот же объект Promise при каждом перерисовании. Если функция fetchUser(userId) создаёт новый объект Promise при каждом перерисовании, React снова приостанавливает его выполнение, что приводит к многократным запросам или к ситуации, когда компонент так и не достигает состояния готовности; в клиентских компонентах React выдаёт предупреждения о создании незакэшированных объектов Promise во время перерисования. Поэтому рассматривайте приведённые примеры лишь как иллюстрации формата. Объект Promise должен поступать от источника, который его кэширует: библиотеки данных с поддержкой Suspense, загрузчиков фреймворков или серверных компонентов, которые инициируют запрос и передают объект Promise в качестве свойства. Иногда рекомендуется оборачивать вызов в useMemo, но React не гарантирует сохранение мемоизированных значений, поэтому это не является надёжным способом кэширования.
Действия на сервере как функция React
Пользователи App Router в Next.js уже знакомы с Server Actions. В React 19 они стали частью самого React (в документации их теперь называют Server Functions) и доступны в любой фреймворке, поддерживающем Server Components.
В примере определена асинхронная функция, помеченная атрибутом 'use server', которая вставляет пользователя и обновляет список, после чего эта функция передается в свойство action формы:
// Server Action — runs on the server, called from the client
async function submitForm(formData) {
'use server';
const name = formData.get('name');
await db.users.create({ name });
revalidatePath('/users');
}
// Client component
function UserForm() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">Add User</button>
</form>
);
}
Свойство action тега <form> теперь принимает функцию, включая асинхронную; React запускает её при отправке формы и передаёт ей объект FormData. Состояние ожидания, оптимистичные обновления и ошибки реализуются с помощью соответствующих API: useFormStatus или useActionState для отслеживания состояния ожидания и результата, useOptimistic для мгновенной обратной связи, а также барьеры ошибок для обработки сбоев. Вы пишете логику изменений; React координирует остальные аспекты.
В реальном приложении необходимо скорректировать одну деталь из этого фрагмента. Функция с встроенной директивой 'use server' может быть определена только в серверном компоненте. Если UserForm является клиентским компонентом, переместите функцию submitForm в отдельный файл, в верхней части которого будет указана директива 'use server', а затем импортируйте этот файл.
Там, где серверные компоненты удаляют статический интерфейс из пакета клиента, серверные действия устраняют вручную написанные маршруты API для выполнения изменений: меньше кода, меньше запросов. Тем не менее каждое действие следует рассматривать как публичный конечный пункт, а внутри него необходимо проверять входные данные и авторизацию.
useOptimistic: мгновенная обратная связь без дублирования состояния
Ранее отрисовка результата до подтверждения с сервера требовала вручную управления дополнительным состоянием. Функция useOptimistic берет текущее состояние и функцию обновления, похожую на редьюсер, возвращает состояние для отрисовки вместе с функцией, применяющей оптимистичные изменения. При добавлении задачи в список создается временная запись с меткой «в ожидании», которая отображается в слегка размытом виде до завершения сохранения:
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
async function handleAdd(text) {
addOptimisticTodo({ id: 'temp', text, pending: true });
await saveTodo(text); // Server Action
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.7 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
При вызове addOptimisticTodo React сразу отображает список в оптимистичном режиме. Когда соответствующая операция завершается, оптимистичные данные удаляются, и React снова отрисовывает список на основе свойства todos, которое к этому моменту должно содержать сохраненную запись.
Раньше вы хранили подтверждённые и ожидающие обработки копии данных и вручную устраняли несоответствия, когда они возникали; теперь этот жизненный цикл контролируется механизмом hook. Важны два условия. Во-первых, оптимистичное обновление должно происходить внутри операции или перехода, например, через функцию, переданную в свойство action формы, или обёрнутую в startTransition; вызов её из обычного обработчика событий приведёт к предупреждению от React, и обновление может не отобразиться. Во-вторых, родительский элемент должен действительно получать обновлённый набор todos после сохранения, обычно в результате повторной верификации; иначе новый элемент исчезнет при сбросе оптимистичного состояния. Временные идентификаторы вроде 'temp' также могут привести к конфликтам, если два элемента добавляются быстро, поэтому необходимо генерировать уникальные идентификаторы. Эти крайние случаи рассматриваются в пяти способах сбоя при использовании оптимистичного отката.
Компилятор React: мемоизация по умолчанию
Компилятор React, ранее называвшийся React Forget, появился вместе с React 19 как опциональный этап сборки и оказал наибольшее влияние на повседневный код. Он встраивает мемоизацию на этапе сборки, благодаря чему значения и функции-обратные вызовы переиспользуются, когда входные данные не изменились, а дочерние элементы пропускают повторную отрисовку при одинаковых свойствах. Ручное использование useMemo, useCallback и React.memo становится в основном ненужным, как показывает следующее сравнение:
// Before: manual memoization required
const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(id), [id]);
const MemoizedChild = React.memo(ChildComponent);
// After React Compiler: write normal code
const expensiveValue = compute(a, b);
const handleClick = () => doSomething(id);
// ChildComponent renders only when its props change - automatically
Вам по-прежнему необходимо понимать, когда и почему компоненты перерисовываются, но ручные способы обхода этой проблемы становятся гораздо менее важными. Наш обзор шаблонов, вызывающих ненужную перерисовку по-прежнему остается полезным справочным материалом.
Для новых проектов компилятор является надежным стандартным решением. В существующем коде его следует внедрять осторожно: он предполагает, что компоненты являются «чистыми» и соответствуют правилам React, поэтому код, изменяющийся во время отрисовки, может вести себя иначе после компиляции. Включайте его постепенно, запускайте тесты и сначала используйте плагин ESLint для выявления нарушений. Проверьте актуальную документацию React, чтобы узнать статус выпуска и поддерживаемые версии React.
Что внедрять и в каком порядке
Немедленная польза, низкий риск
useOptimisticтам, где формы взаимодействуют со состоянием сервера.- Server Actions, если вы используете Next.js 14 или новее и хотите заменить API-маршруты для выполнения операций изменения данных.
Что стоит запланировать
use, когда ваш слой данных генерирует стабильные, закэшированные Promises; он хорошо сочетается с Suspense.
- Использование компилятора React в новых проектах или сначала в хорошо протестированных частях существующего приложения.
Для большинства приложений это не срочно
- Изменения в объекте ref: теперь
refможно передавать как обычное свойство функциональным компонентам, поэтомуforwardRefбольше не требуется. - Улучшения в работе с контекстом, которые в основном касаются улучшения опыта разработчика, а не добавления нового поведения.
- Поддержка метаданных документа, что позволяет компонентам отображать теги
<title>и<meta>, которые React вставляет в начало документа.
Основные выводы
- React 19 — это развитие существующей версии, а не радикальная переработка. Его новые API формализуют подходы, которые команды по разработке уже используют вручную: оптимистичные обновления, изменения на сервере и условное чтение данных.
use переносит обработку загрузки и ошибок в Suspense и границы ошибок, но эффективно работает только с Promises, которые кэшируются вне процесса отрисовки.useOptimistic хорошо сочетаются друг с другом: первый обрабатывает запись данных, а второй скрывает свою задержку.Важный вопрос заключается не в том, стоит ли обновляться, а в том, какой из этих инструментов заменит временное решение, которое вы сейчас используете. Начните с этого.
Связанные материалы
- useOptimistic Rollback: Пять режимов сбоя в Server Actions Next.js — Узнайте, почему функция useOptimistic бесшумно восстанавливает интерфейс, не объясняя пользователю причины сбоев, на примере пяти проверенных режимов сбоя Server Actions и рабочего решения.
- Server Actions или Route Handlers? Руководство по принятию решений для Next.js 16 — Узнайте, когда операция в Next.js 16 следует реализовывать с помощью Server Actions, а когда требуется Route Handler, с исправленными примерами для форм, ошибок, механизма оптимизма и вебхуков.