Controlled или uncontrolled: форма React с текстом, чекбоксом и файлом
- 2 октября 2026
В форме профиля имя меняет предпросмотр, чекбокс включает рассылку, а файл выбирает пользователь. Имя и чекбокс удобно связать с состоянием React. Поле файла оставьте браузеру: при отправке все значения можно собрать через FormData.
Управляемое поле, или controlled, получает текущее значение из React. У неуправляемого, uncontrolled, текущее значение хранит DOM. Выбирайте модель для каждого поля отдельно: одна форма может сочетать обе модели.
| Что нужно от поля | Подход | Пара свойств |
|---|---|---|
| Имя сразу видно в предпросмотре | Управляемое текстовое поле | value и onChange |
| От отметки зависит сообщение рядом | Управляемый чекбокс | checked и onChange |
| Значение понадобится при отправке | Неуправляемое поле | defaultValue или defaultChecked |
| Пользователь выбирает файл | Нативное неуправляемое поле | type="file", без value |
Если JSX и useState пока незнакомы, начните с нашего введения в React. Здесь мы уже собираем работающую форму в существующем клиентском проекте с React и обработкой JSX. Например, в проекте на Vite подойдут файлы src/App.jsx и src/main.jsx; в index.html нужен <div id="root"></div>.
Соберите форму с предпросмотром
Сохраните этот компонент в src/App.jsx. Когда вы введёте имя, надпись под полем сразу изменится, а после нажатия «Показать данные» под формой появится снимок значений. Сетевых запросов в примере нет.
import { useState } from 'react';
export default function App() {
const [displayName, setDisplayName] = useState('');
const [newsletter, setNewsletter] = useState(false);
const [result, setResult] = useState(null);
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
data.set('newsletter', newsletter ? 'yes' : 'no');
const avatar = data.get('avatar');
setResult({
displayName: data.get('displayName'),
newsletter: data.get('newsletter'),
avatar: avatar instanceof File && avatar.name !== ''
? { name: avatar.name, size: avatar.size, type: avatar.type }
: null,
});
}
return (
<>
<form onSubmit={handleSubmit}>
<label>
Имя в профиле
<input
name="displayName"
value={displayName}
onChange={(event) => setDisplayName(event.currentTarget.value)}
required
/>
</label>
<p>Предпросмотр: {displayName || 'имя пока не задано'}</p>
<label>
<input
name="newsletter"
type="checkbox"
checked={newsletter}
onChange={(event) => setNewsletter(event.currentTarget.checked)}
/>
Получать новости
</label>
<p>Рассылка: {newsletter ? 'подключена' : 'отключена'}</p>
<label>
Аватар
<input name="avatar" type="file" accept="image/png,image/jpeg" />
</label>
<button type="submit">Показать данные</button>
</form>
{result !== null && (
<pre aria-label="Данные формы">{JSON.stringify(result, null, 2)}</pre>
)}
</>
);
}
В src/main.jsx подключите компонент к странице:
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('root');
if (container === null) {
throw new Error('В index.html нужен элемент с id="root"');
}
createRoot(container).render(<App />);
Введите «Енот», отметьте рассылку и нажмите кнопку. В результате появятся displayName: "Енот", newsletter: "yes" и avatar: null, если файл ещё не выбран. Поле имени обязательное: обычная отправка через кнопку с пустым именем остановится на встроенной проверке браузера.
Обновляйте имя через value, отметку через checked
Для текста React получает строку displayName в value. Когда пользователь вводит букву, обработчик onChange читает новое значение и сразу вызывает setDisplayName, после чего поле и предпросмотр получают одну и ту же строку. Они меняются вместе. Держите обновление значения в самом обработчике: отложенная запись через таймер мешает обычному вводу.
Перед разбором чекбокса предскажите, что вернёт event.currentTarget.value, если поставить отметку, а затем снять её. В форме выше у чекбокса не задан value. Запишите ответ и объясните, какое свойство вы прочитали бы для проверки самой отметки; после этого сверьтесь с разбором ниже.
У чекбокса другая пара: checked={newsletter} и event.currentTarget.checked. Свойство checked содержит булево значение. В нашем примере value вернёт строку "on" и при поставленной, и при снятой отметке: это значение для отправки формы. Саму отметку оно не показывает. Эти правила разобраны в документации React для <input>.
Почему состояния начинаются с '' и false? Поля получают строку и булево значение с первого рендера. Так React сохраняет выбранный режим.
Вот намеренно ошибочный фрагмент для сравнения:
// Ошибка: сначала value будет undefined, после ввода — строкой.
const [displayName, setDisplayName] = useState();
Если передать это состояние в value из полной формы, React предупредит о переходе из uncontrolled в controlled. Задайте useState(''). Для данных, которые приходят позже, можно передавать value={displayName ?? ''}. Поле, которое сознательно показывают только для чтения, оформляют с readOnly.
Прочитайте файл при отправке
У поля аватара нет value. Выбранный файл хранится в нативном поле; при необходимости его также можно прочитать через input.files. Программно задать файловому полю непустое value нельзя: браузер выбросит InvalidStateError. Через input.value = '' можно очистить выбор. Поэтому имя файла не подходит для управления полем так же, как текстовым вводом. Это ограничение задано в стандарте HTML; поведение поля также описано в справке MDN.
В обработчике new FormData(event.currentTarget) собирает значения формы. currentTarget здесь указывает на <form>, где назначен onSubmit. Поля попадают в набор под своими name. Неотмеченный чекбокс отсутствует, поэтому мы явно записываем yes или no через data.set. Это правило FormData действует независимо от модели React.
У невыбранного файлового поля может оказаться пустой File с пустым именем. По avatar.name !== '' отличайте его от выбранного файла. Одного условия size > 0 недостаточно: пользователь может выбрать файл нулевого размера. Отключённые поля с disabled в набор не попадут. См. правила работы FormData.
Результат под формой содержит только описание файла: имя, размер в байтах и тип. Содержимое файла остаётся в data. Для настоящей загрузки на сервер передают сам FormData в тело запроса, а формат и адрес берут из контракта API. accept подсказывает фильтр в окне выбора; проверку допустимых файлов на сервере он не заменяет.
Если добавите асинхронную отправку, вызовите preventDefault и соберите FormData до первого await. Тогда дальнейшему коду не потребуется читать event.currentTarget после завершения обработчика.
Пусть DOM управляет полями, которые нужны только при отправке
Предпросмотр нужен не каждой форме. Для короткой формы, значения которой читаются при отправке, начните с неуправляемых полей. Тогда не придётся хранить в состоянии каждую букву.
Ниже отдельный компонент QuickForm.jsx. Чтобы проверить его вместо предыдущей формы, импортируйте QuickForm в main.jsx и передайте <QuickForm /> в render.
import { useState } from 'react';
export default function QuickForm() {
const [result, setResult] = useState('');
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
setResult(`${data.get('displayName')}: ${data.has('newsletter')}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Имя <input name="displayName" defaultValue="Енот" />
</label>
<label>
<input name="newsletter" type="checkbox" defaultChecked />
Получать новости
</label>
<button type="submit">Прочитать поля</button>
<p aria-label="Результат короткой формы">{result}</p>
</form>
);
}
defaultValue задаёт начальный текст, defaultChecked — начальную отметку. Дальше вводом управляет DOM. Если при обычном повторном рендере передать другой defaultValue, это не будет командой заменить текст, который пользователь уже набрал в поле. Состояние нашей формы хранит только последний результат отправки.
Смените имя на «Выдра», уберите отметку и отправьте форму. Строка результата станет Выдра: false; имя в поле сохранится после рендера с результатом.
Найдите причину странного ввода
| Симптом | Что проверить |
|---|---|
| Буквы не появляются или возвращаются назад | Есть ли onChange и обновляет ли он состояние для value? |
| Чекбокс не переключается | Читаете ли вы checked, обновляете ли состояние для него? |
| React сообщает о смене режима | Не было ли в value сначала undefined или null? |
| Поле пропало из FormData | Есть ли у него name, не стоит ли disabled? Для чекбокса проверьте отметку. |
| Файл выглядит как пустой объект в JSON | Вы сериализовали объект File. Для загрузки оставьте файл в FormData. |
Добавьте поле и проверьте свой выбор
Доработайте первую форму: добавьте адрес почты с name="email" и type="email". Сначала оставьте поле неуправляемым. Подсказка: читать его можно через data.get('email'), а выводить в объекте результата вместе с именем и признаком подписки на рассылку. Адрес появится после нажатия кнопки.
Самостоятельная вариация: покажите введённый адрес рядом с полем до отправки и проверьте, как надпись меняется, когда пользователь стирает всё содержимое поля. Для этого понадобится состояние с начальной пустой строкой.
Почему отметка чекбокса управляется через checked, хотя в FormData у поля есть строковое значение? Как изменится чтение полей, если вы уберёте предпросмотр и будете получать все данные только при отправке?
Если хотите продолжить практику с компонентами и состоянием, посмотрите программу курса HTML Academy «React. Разработка сложных клиентских приложений».
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.