В этой статье рассматриваются распространенные трудности, с которыми сталкиваются разработчики при работе с формами, и как React 19 наконец представляет долгожданные инструменты, которые делают обработку форм более чистой, декларативной и менее подверженной ошибкам.
За последние шесть лет во фронтенд-разработке — от создания сложных систем форм до интеграции ИИ-инструментов в SDG — я написал, отладил и рефакторил больше кода форм, чем хотелось бы признать.
И если вы когда-либо создавали или поддерживали формы в React, вы, вероятно, разделяете это чувство. Они обманчиво просты... пока не становятся сложными.
В этой статье я расскажу о распространенных трудностях, с которыми сталкиваются разработчики при работе с формами, и о том, как React 19 наконец представляет долгожданные инструменты, которые делают обработку форм более чистой, декларативной и менее подверженной ошибкам. ✨
🔍 Давайте начнем с болевых точек, с которыми сталкивался каждый React-разработчик хотя бы раз.
Управление состоянием формы в React обычно начинается так:
const [name, setName] = useState(''); const [surname, setSurname] = useState(''); const [error, setError] = useState(null); function handleSubmit(event) { event.preventDefault(); }
✅ Это просто — и вполне подходит для небольших форм.
Но как только вы масштабируетесь, вы утопаете в повторяющихся хуках состояния, ручных сбросах и бесконечных вызовах event.preventDefault().
Каждое нажатие клавиши вызывает повторный рендеринг, а управление ошибками или состояниями ожидания требует еще больше переменных состояния. Это функционально, но далеко не элегантно.
Когда ваша форма — это не просто один компонент, а иерархия вложенных компонентов, вы передаете пропсы через каждый уровень:
<Form> <Field error={error} value={name} onChange={setName}> <Input /> </Field> </Form>
Состояние, ошибки, флаги загрузки — все просверливается через несколько слоев. 📉 \n Это не только раздувает код, но и делает обслуживание и рефакторинг болезненными. 😓
Пробовали ли вы когда-нибудь реализовать оптимистичные обновления вручную?
Это когда вы показываете "успешное" изменение в UI сразу после действия пользователя — до того, как сервер фактически подтвердит его.
Звучит просто, но управление логикой отката при сбое запроса может быть настоящей головной болью. 🤕
Где хранить временное оптимистичное состояние? Как объединить, а затем откатить его? 🔄
React 19 представляет нечто гораздо более чистое для этого.
Одно из самых захватывающих дополнений в React 19 — это хук ==*useActionState *==.
Он упрощает логику форм, объединяя асинхронную отправку формы, управление состоянием и индикацию загрузки — все в одном месте. 🎯
const [state, actionFunction, isPending] = useActionState(fn, initialState);
Вот что происходит:
==fn== — ваша асинхронная функция, которая обрабатывает отправку формы
==initialState== — начальное значение состояния вашей формы
==isPending== — встроенный флаг, показывающий, выполняется ли отправка
\
Асинхронная функция, переданная в ==useActionState== автоматически получает два аргумента:
const action = async (previousState, formData) => { const message = formData.get('message'); try { await sendMessage(message); return { success: true, error: null }; } catch (error) { return { success: false, error }; } };
Затем вы подключаете её к своей форме так:
const [state, actionFunction, isPending] = useActionState(action, { success: false, error: null, }); return <form action={actionFunction}> ... </form>;
\n Теперь, когда форма отправляется, React автоматически:
Больше никаких ручных ==useState, preventDefault,== или логики сброса — React заботится обо всем этом. ⚙️
Если вы решите запустить действие формы вручную (например, вне свойства action формы), оберните его в ==startTransition==:
const handleSubmit = async (formData) => { await doSomething(); startTransition(() => { actionFunction(formData); }); };
В противном случае React предупредит вас, что асинхронное обновление произошло вне перехода, и ==isPending== не будет правильно обновляться.
Логика формы снова ощущается декларативной — просто опишите действие, а не проводку.
Еще один мощный новый хук — ==useFormStatus== — решает проблему сверления пропсов в деревьях форм.
import { useFormStatus } from 'react-dom'; const { pending, data, method, action } = useFormStatus();
Вы можете вызвать этот хук внутри любого дочернего компонента формы, и он автоматически подключится к состоянию родительской формы.
function SubmitButton() { const { pending, data } = useFormStatus(); const message = data ? data.get('message') : ''; return ( <button type="submit" disabled={pending}> {pending ? `Sending ${message}...` : 'Send'} </button> ); } function MessageForm() { return ( <form action={submitMessage}> <SubmitButton /> </form> ); }
:::info Обратите внимание, что ==SubmitButton== может получить доступ к данным формы и статусу ожидания — без передачи каких-либо пропсов вниз.
:::
🧩 Устраняет сверление пропсов в деревьях форм \n ⚡ Делает возможными контекстные решения внутри дочерних компонентов \n 💡 Сохраняет компоненты несвязанными и более чистыми
Наконец, давайте поговорим об одном из моих любимых дополнений — ==useOptimistic==.
Он обеспечивает встроенную поддержку оптимистичных обновлений UI, делая взаимодействие с пользователем мгновенным и плавным.
Представьте, что вы нажимаете "Добавить в избранное". Вы хотите показать обновление немедленно — до ответа сервера.
Традиционно вам пришлось бы жонглировать между локальным состоянием, логикой отката и асинхронными запросами.
С ==useOptimistic== это становится декларативным и минимальным:
const [optimisticMessages, addOptimisticMessage] = useOptimistic( messages, (state, newMessage) => [newMessage, ...state] ); const formAction = async (formData) => { addOptimisticMessage(formData.get('message')); try { await sendMessage(formData); } catch { console.error('Failed to send message'); } };
Если запрос к серверу не удается, React автоматически возвращается к предыдущему состоянию.
Если он успешен — оптимистичное изменение остается.
Функция обновления, которую вы передаете в useOptimistic, должна быть чистой:
❌ Неправильно:
(prev, newTodo) => { prev.push(newTodo); return prev; }
✅ Правильно:
(prev, newTodo) => [...prev, newTodo];
:::tip Всегда возвращайте новый объект или массив состояния!
:::


