Формы в React
Контролируемые и неконтролируемые, FormData, useActionState, useFormStatus, доступные ошибки.
Обновлено
Что значит форма в React
У формы три вещи, которыми нужно управлять: значения полей, ошибки валидации и состояние отправки (pending, успех, ошибка сервера). React даёт два базовых способа хранить значения, и все библиотеки форм построены на одном из них.
База HTML (name, label, типы полей, нативная валидация) — в уроке Формы HTML; здесь мы строим поверх неё.
Контролируемые и неконтролируемые
Разница между контролируемым и неконтролируемым полем:
| Контролируемое | Неконтролируемое | |
|---|---|---|
| Где хранится значение | в состоянии React (useState) |
в DOM (поле хранит его само) |
| Как его прочитать | из переменной, в любой момент | при отправке: FormData или ref |
| Перерисовка на каждое нажатие | да | нет |
| Код | value + onChange на каждом поле |
только name (+ defaultValue) |
| Когда | значение влияет на интерфейс в реальном времени: живой поиск, зависимые поля, форматирование (телефон, карта) | большинство форм: вход, контакты, настройки |
// контролируемое
const [email, setEmail] = useState('')
<input value={email} onChange={e => setEmail(e.target.value)} />
// неконтролируемое
<input name="email" defaultValue={user.email} />value без onChange = заблокированное поле (только чтение). defaultValue = начальное значение, дальше управляет DOM.
Чтение данных: FormData
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const form = new FormData(e.currentTarget)
const email = String(form.get('email'))
const tags = form.getAll('tags') // чекбоксы с одним name → массив
const data = Object.fromEntries(form) // ⚠️ для повторяющихся name сохраняет только последнее значение
}Значения FormData — всегда строки (или File). Отмеченный чекбокс отправляет 'on', неотмеченный не появляется вообще.
React 19: формы с действиями
React 19 позволяет передать функцию прямо в action. React сам займётся preventDefault, FormData и состоянием ожидания.
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
type State = { error?: string; ok?: boolean }
async function subscribe(prev: State, formData: FormData): Promise<State> {
const email = String(formData.get('email') ?? '')
if (!email.includes('@')) return { error: 'Неверный email' }
await api('/subscribe', { method: 'POST', body: formData })
return { ok: true }
}
function SubmitButton() {
const { pending } = useFormStatus() // читает состояние родительской формы
return <button disabled={pending}>{pending ? 'Отправка…' : 'Подписаться'}</button>
}
export function Newsletter() {
const [state, action] = useActionState(subscribe, {})
return (
<form action={action}>
<input name="email" type="email" required />
{state.error && <p role="alert">{state.error}</p>}
<SubmitButton />
</form>
)
}| Хук | Что даёт | Где |
|---|---|---|
useActionState(fn, initial) |
[state, action, isPending] — результат последней отправки |
компонент с <form> |
useFormStatus() |
{ pending, data } родительской формы |
дочерний компонент формы |
useOptimistic |
интерфейс, обновлённый до ответа | списки, лайки |
С Server Action вместо клиентской функции та же форма работает даже без JavaScript.
Доступные ошибки
Ошибку нужно видеть и слышать:
<label htmlFor="email">Email</label>
<input id="email" name="email" aria-invalid={!!error} aria-describedby={error ? 'email-error' : undefined} />
{error && <p id="email-error" role="alert">{error}</p>}| Атрибут | Роль |
|---|---|
aria-invalid |
скринридер объявляет «invalid»; стилизуете через aria-invalid:border-danger |
aria-describedby |
связывает сообщение с полем — его прочитают, когда пользователь дойдёт до поля |
role="alert" / aria-live |
сообщение объявляется сразу при появлении |
Когда нужна библиотека
| В форме... | Без библиотеки | С библиотекой (React Hook Form / Conform) |
|---|---|---|
| 2–5 полей, простая валидация | ✓ useActionState + FormData |
не нужно |
| валидация при blur / вводе, сообщения по полям | громоздко | ✓ |
| динамические списки (добавить / удалить строки) | громоздко | ✓ |
| многошаговый мастер, зависимые поля | громоздко | ✓ |
Коротко
- Неконтролируемые +
FormDataдля большинства форм; контролируемые — когда значение меняет интерфейс вживую. - React 19:
action={fn},useActionStateдля результата,useFormStatusдля ожидания. - Ошибки:
aria-invalid+aria-describedby+role="alert".