Асинхронные формы в React 19 с use, useActionState и useOptimistic
Как функции use(), useActionState, useFormStatus и useOptimistic заменяют ручную загрузку данных и флаги ошибок в React 19, а также какие подводные камни тайно скрывает каждый из этих хуков.
Источники: React 19’s New Hooks: A Practical Guide
До React 19 практически каждый запрос или форма требовали одинаковой структуры: useState для хранения данных, useEffect для их загрузки, а также флаги для отображения состояния загрузки, ошибок и результатов. React 19 вводит четыре API, которые переносят эту логику в сам фреймворк: use(), useActionState, useFormStatus и useOptimistic. Здесь описано, что делает каждый из них, как они взаимодействуют между собой, а также ограничения, которые легко упустить. Для более полного обзора обновлений см. React 19 upgrade triage.
Чтение обещаний во время отрисовки с помощью use()
use() — это не хук в традиционном смысле. В отличие от хуков, его можно вызывать внутри условий, циклов или после досрочного возврата. Он считывает значение Promise или Context во время отрисовки и сотрудничает с Suspense для отображения состояния ожидания.
import { use, Suspense } from "react";
UserCard принимает promise и распаковывает его с помощью use; пока он не реализован, ближайший элемент Suspense в Page отображает свой запасной вариант.
function UserCard({ userPromise }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}function Page({ userPromise }) {
return (
<Suspense fallback={<p>Loading user…</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
Родительский компонент создаёт promise, дочерний компонент им пользуется, а Suspense отвечает за отображение состояния загрузки. Существует одно важное ограничение: promise должен быть стабильным. Promise, созданный во время отрисовки клиентского компонента, становится новым при каждой отрисовке и приводит к повторному вводу компонента в состояние ожидания, поэтому его следует создавать в серверном компоненте, в кэшируемом загрузчике или через API данных фреймворка.
Обработка отправки данных с useActionState
useActionState заменяет вручную управляемые состояния isSubmitting, error и result. Вы передаёте действие и начальное состояние; в результате получаете текущее состояние, обёрнутое действие для формы и флаг ожидания. Действие получает предыдущее состояние и объект FormData, а то, что оно возвращает, становится следующим состоянием.
import { useActionState } from "react";
Здесь submitFeedback проверяет сообщение, сохраняет его и возвращает либо маркер ошибки, либо маркер успешного выполнения, который форма отображает непосредственно.
async function submitFeedback(prevState, formData) {
const message = formData.get("message");
if (message.length < 5) {
return { error: "Message is too short." };
}
await saveFeedback(message);
return { success: true };
}function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {}); return (
<form action={formAction}>
<textarea name="message" required />
<button disabled={isPending}>
{isPending ? "Sending…" : "Send feedback"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p>Thanks for the feedback!</p>}
</form>
);
}
Поскольку действие передаётся в стандартное атрибут формы action, это обеспечивает прогрессивное улучшение функционала. Обратите внимание, что отправка данных до их обработки работает только тогда, когда действие представляет собой серверную функцию; обычная клиентская функция требует загрузки JavaScript. Кроме того, метод formData.get возвращает null для отсутствующего поля, поэтому в реальном коде необходимо учитывать этот случай.
Чтение статуса формы из любого потомка с помощью useFormStatus
Кнопка отправки, расположенная глубоко внутри формы, часто должна знать, выполняется ли отправка данных, и передача свойства isPending через каждый уровень структуры является громоздкой. Функция useFormStatus, импортируемая из react-dom, позволяет получить статус ближайшего родителя-элемента <form>.
import { useFormStatus } from "react-dom";
SubmitButton не принимает никаких параметров; он берет информацию о состоянии в ожидании из формы, в которой находится, что делает его идеальным решением для компонентов кнопок, используемых совместно.
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}function SettingsForm({ action }) {
return (
<form action={action}>
<input name="username" />
<SubmitButton />
</form>
);
}
Хук необходимо вызывать в компоненте, отрисовываемом внутри <form>. Если его вызывать в компоненте, отрисовывающем саму форму, информация о состоянии в ожидании не будет получена.
Раннее отображение ожидаемых результатов с useOptimistic
useOptimistic сразу отображает предполагаемый результат действия во время выполнения реального запроса.
import { useOptimistic, useRef } from "react";
В TodoList хук формирует массив optimisticTodos на основе параметра todos, добавляя элементы с меткой pending: true. Действие формы добавляет такой оптимистичный элемент, сбрасывает форму с помощью ref, а затем ожидает реального ответа от сервера. Элементы в состоянии ожидания отображаются с полупрозрачностью.
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const formRef = useRef(null); async function handleSubmit(formData) {
const title = formData.get("title");
addOptimisticTodo({ id: Date.now(), title });
formRef.current?.reset();
await addTodoAction(formData);
} return (
<div>
<form ref={formRef} action={handleSubmit}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
Модель мышления ясна: оптимистичное состояние существует только во время выполнения действия. Как только оно завершается, React удаляет его и отображает то, что в настоящее время хранится в переменной todos. При успешном выполнении родительский компонент должен предоставить обновленные данные todos (например, путем повторной валидации), иначе элемент исчезает; при неудаче он автоматически удаляется, что и является механизмом отката. Сообщения об ошибках также требуют обработки. Статья о способах обработки сбоев при использовании оптимистичного отката рассматривает крайние случаи.
Бонус: ref как обычное свойство
В React 19 функциональные компоненты также могут принимать ref в качестве обычного свойства, поэтому в большинстве случаев forwardRef не требуется. Это позволяет избавиться от множества слоев оберток в библиотеках компонентов.
Основные выводы
use()разворачивает обещания и контекст во время отрисовки с использованием Suspense, но обещание должно быть стабильным.useActionStateобъединяет состояние, результат и флаг ожидания в один хук, основанный на форме.useFormStatusсчитывает статус внешней формы из её потомка, устраняя необходимость прохождения по свойствам вверх.useOptimisticотображает ожидаемые результаты во время выполнения действия и опирается на свежие свойства для их сохранения.- Перед пересозданием комбинации
useStateиuseEffectпроверьте, подходит ли к случаю один из этих вариантов.