Асинхронні форми в React 19 з use, useActionState та useOptimistic
Як use(), useActionState, useFormStatus та useOptimistic замінюють ручне завантаження та позначки помилок у React 19, разом із проблемами, які кожен хук тихо приховує.
До 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 (наприклад, шляхом перевірки), інакше елемент зникає; у разі невдачі він автоматично зникає, що і є механізмом відкату. Повідомлення про помилки все одно потребують обробки. Стаття про режими невдач useOptimistic rollback розглядає крайні випадки.
Бонус: ref як звичайний проп
React 19 також дозволяє функційним компонентам отримувати ref як звичайний проп, тому у більшості випадків forwardRef є зайвим. Це усуває багато надмірних обгорток у бібліотеках компонентів.
Ключові висновки
use()розпаковує обіцянки та контекст під час відображення за допомогою Suspense, але обіцянка має бути стабільною.useActionStateоб’єднує стан, результат та флаг очікування в один хук, заснований на формі.useFormStatusчитає статус зовнішньої форми з її нащадка, усуваючи необхідність передачі значень через кілька рівнів.useOptimisticвідображає очікувані результати під час виконання дії та спирається на свіжі значення пропсів для їх збереження.- Перш ніж перебудувати комбінацію
useStateтаuseEffect, перевірте, чи підходить один із цих варіантів.