У цій статті розглядаються поширені проблеми, з якими стикаються розробники при роботі з формами, і як 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 Це не тільки роздуває код, але й робить обслуговування та рефакторинг болісними. 😓
Ви коли-небудь намагалися реалізувати оптимістичні оновлення вручну?
Це коли ви показуєте "успішну" зміну в інтерфейсі відразу після дії користувача — до того, як сервер фактично підтвердить її.
Звучить просто, але управління логікою відкату, коли запит не вдається, може бути справжнім головним болем. 🤕
Де зберігати тимчасовий оптимістичний стан? Як об'єднати, а потім відкотити його? 🔄
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==.
Він забезпечує вбудовану підтримку оптимістичних оновлень інтерфейсу, роблячи взаємодію користувача миттєвою та плавною.
Уявіть, що ви натискаєте "Додати до обраного". Ви хочете показати оновлення негайно — до відповіді сервера.
Традиційно вам довелося б жонглювати між локальним станом, логікою відкату та асинхронними запитами.
З ==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 Завжди повертайте


