open atlas
↑ К треку
Паттерны React RXP · 12 · 01

Управляемые и неуправляемые формы

Управляемые поля отдают значение React (value + onChange) ради живой валидации или зависящего от значения UI; неуправляемые отдают значение DOM, читаются раз через FormData — меньше перерисовок и кода. По умолчанию — неуправляемые для простых форм только с отправкой.

RXP Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Рефлекс, выработанный на треке React, — подключать каждое поле как value={x} onChange={...}. Это ощущается как «правильный React-путь»: состояние внутрь, состояние наружу, полный контроль. И вот форма регистрации получает восемь вызовов useState и восемь обработчиков onChange, а каждое нажатие клавиши в любом поле перерисовывает весь компонент формы — включая те семь полей, которых пользователь не касается.

Бо́льшая часть этого контроля не покупает ничего. У формы есть один значимый момент — отправка, — и значения нужны только тогда. На виду прячется более экономный вариант по умолчанию: пусть DOM держит текст, а ты прочитаешь его один раз при отправке формы. Знать, какие именно поля действительно требуют, чтобы React владел их значением во время ввода, — это и есть senior-решение, о котором этот урок.

Цель

После этого урока ты можешь отличить управляемое поле (React владеет значением через value + onChange, перерисовываясь на каждое нажатие) от неуправляемого (значением владеет DOM, читается при отправке через FormData или ref); выбирать управляемое только тогда, когда значение движет другим UI или требует живой валидации; по умолчанию брать неуправляемое + FormData для простых форм только с отправкой; прочитать action формы React 19, которому FormData достаётся даром; и назвать режим отказа — управление каждым полем по рефлексу и перерисовку большой формы на каждое нажатие.

1

Управляемое означает, что React — единственный источник истины для значения: value плюс onChange и перерисовка на каждое нажатие. Поле показывает ровно то, что говорит состояние; ввод вызывает onChange, ты обновляешь состояние, React перерисовывает, поле показывает новое значение. Этот цикл туда-обратно — и есть смысл: поскольку состояние отражает поле на каждой клавише, ты можешь реагировать на значение пока пользователь печатает — валидировать его, преобразовывать, двигать им другой UI.

function ControlledEmail() {
  const [email, setEmail] = useState("");
  const invalid = email.length > 0 && !email.includes("@");
  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {invalid && <p role="alert">Email needs an @</p>}
    </>
  );
}

Цена явная: каждое нажатие — это обновление состояния и рендер. Для одного поля это ничто. Для формы из 20 полей, где всё состояние сидит в одном компоненте, каждая клавиша перерисовывает все 20.

2

Неуправляемое означает, что значением владеет DOM — ты его не отслеживаешь, а читаешь один раз при отправке. Никакого value, никакого onChange, никакого состояния, никакого рендера на каждое нажатие. Браузер делает то, что браузеры делали всегда: держит текст в поле. Начальное значение задаёшь через defaultValue (не value), а финальное забираешь, когда оно нужно — при отправке.

function UncontrolledEmail() {
  const ref = useRef<HTMLInputElement>(null);
  function submit() {
    console.log(ref.current?.value); // read once, on demand
  }
  return <input ref={ref} defaultValue="" name="email" />;
}

Ввод здесь вызывает ноль рендеров React — компонент рендерится один раз и остаётся на месте, пока пользователь заполняет всю форму. Это более экономный вариант по умолчанию, и для большинства форм он же и меньше кода: ни состояния, ни обработчика на поле.

3

Для форм только с отправкой пропусти и реф — прочитай все поля сразу через FormData. ref на каждое поле нормален для одного-двух полей, но это всё ещё бойлерплейт, растущий линейно. Браузер уже собирает за тебя каждое именованное поле. Дай каждому полю name, повесь один onSubmit на <form> и собери FormData из элемента формы. Один обработчик, любое число полей, никакой обвязки на каждое поле.

function SignupForm() {
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    const email = data.get("email");      // typed values, read once
    const password = data.get("password");
    // ...send to the server
  }
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" defaultValue="" />
      <input name="password" type="password" />
      <button>Sign up</button>
    </form>
  );
}

Десять полей стоят столько же, сколько два: ни лишнего состояния, ни лишних рефов, ни лишних рендеров. Это паттерн, к которому простая форма должна тянуться первым.

4

React 19 делает неуправляемый вариант по умолчанию полноправным: action формы получает FormData напрямую. Вместо onSubmit + preventDefault + ручного new FormData ты передаёшь функцию в проп action формы, и React вызывает её с заполненным FormData. Соедини это с useActionState — и получишь состояние ожидания и возвращённый результат, не управляя ни единым полем: фреймворк построен вокруг формы «неуправляемое-плюс-FormData», а не управляемой.

"use client";
import { useActionState } from "react";

async function signup(_prev: string | null, formData: FormData) {
  const email = formData.get("email");
  // call the server, return an error string or null
  return email ? null : "Email is required";
}

function SignupForm() {
  const [error, action, pending] = useActionState(signup, null);
  return (
    <form action={action}>
      <input name="email" type="email" />
      <button disabled={pending}>{pending ? "..." : "Sign up"}</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

Заметь: ни одно из этих полей не управляемо. Нужное значение приходит в formData ровно в тот момент, когда оно тебе нужно.

Разбор примера

Форма профиля, управляемая по рефлексу, а затем исправленная. Вариант «до» управляет всеми пятью полями в одном компоненте — стандартная ошибка senior-трека.

// before: every keystroke re-renders all five fields
function ProfileForm() {
  const [name, setName] = useState("");
  const [bio, setBio] = useState("");
  const [city, setCity] = useState("");
  const [site, setSite] = useState("");
  const [phone, setPhone] = useState("");
  return (
    <form onSubmit={/* read 5 state vars */ () => {}}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <textarea value={bio} onChange={(e) => setBio(e.target.value)} />
      <input value={city} onChange={(e) => setCity(e.target.value)} />
      <input value={site} onChange={(e) => setSite(e.target.value)} />
      <input value={phone} onChange={(e) => setPhone(e.target.value)} />
      <button>Save</button>
    </form>
  );
}

Ни одно из этих значений не движет другим UI и не требует живой валидации. Они читаются только при сохранении — так что контроль здесь чистые накладные расходы: пять useState, пять обработчиков, полная перерисовка на каждую клавишу в любом поле. Исправление — неуправляемое + FormData.

// after: zero state, zero per-keystroke renders
function ProfileForm() {
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const d = new FormData(e.currentTarget);
    save({
      name: d.get("name"),
      bio: d.get("bio"),
      city: d.get("city"),
      site: d.get("site"),
      phone: d.get("phone"),
    });
  }
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" defaultValue="" />
      <textarea name="bio" defaultValue="" />
      <input name="city" defaultValue="" />
      <input name="site" defaultValue="" />
      <input name="phone" defaultValue="" />
      <button>Save</button>
    </form>
  );
}

То же поведение, меньше строк, и компонент рендерится ровно один раз за всю сессию редактирования. Теперь представь, что одному полю действительно нужна живая обратная связь — имя пользователя, показывающее «занято / свободно» по мере ввода. Ты сделаешь управляемым только это поле и оставишь остальные четыре неуправляемыми. Это и есть senior-позиция: неуправляемое по умолчанию, управляемое точечно там, где значение действительно нужно во время ввода.

Почему это работает

Почему перерисовка на каждое нажатие обычно нормальна, пока не перестаёт ею быть? Рендеры React по отдельности дёшевы, и управляемое поле на маленькой форме незаметно. Цена проявляется, когда всё состояние живёт в одном большом компоненте: один setState перерисовывает этот компонент и всё его поддерево, так что одно нажатие может перерисовать десятки полей плюс любых дорогих соседей (живой предпросмотр, график, тяжёлый список). Управляемую форму можно спасти, колоцируя состояние каждого поля в его собственном дочернем компоненте, чтобы нажатие перерисовывало только это поле, — но если значение во время ввода тебе вовсе не нужно, неуправляемое снимает весь вопрос.

Частая ошибка

Режим отказа — управление каждым полем по рефлексу. На форме, которую ты строишь сегодня, это выглядит безобидно, а потом кто-то добавляет панель живого предпросмотра или форма вырастает до 30 полей — и вдруг каждое нажатие дёргается, потому что на каждую клавишу идёт полная перерисовка. Признак — компонент формы, набитый парами useState/onChange, чьи значения читаются только внутри onSubmit. Если значение читается только при отправке, оно никогда не должно было быть состоянием. Тянись к управляемому, когда значение движет другим UI или требует валидации по мере ввода, — иначе пусть DOM его держит.

Проверь себя
Викторина

Ты строишь форму обратной связи: имя, email, сообщение — значения читаются только когда пользователь нажимает «Отправить», без живой валидации и без чего-либо ещё на странице, зависящего от них. Каков senior-вариант по умолчанию?

Итог

Управляемое поле делает React источником истины — value + onChange, перерисовка на каждое нажатие, — что нужно только тогда, когда значение движет другим UI или требует живой валидации по мере ввода. Неуправляемое поле отдаёт значение DOM: задай defaultValue, затем прочитай его раз при отправке через ref или, лучше для многополевых форм, одним FormData по именованным полям. React 19 опирается на это — action формы получает FormData напрямую и сочетается с useActionState ради состояния ожидания и результата, всё это без управления единым полем. Senior-вариант по умолчанию — неуправляемое + FormData для простых форм только с отправкой (меньше кода, куда меньше рендеров) и управляемое точечно для одного-двух полей, которым значение действительно нужно во время ввода. Режим отказа — обратный рефлекс, управлять всем, — он перерисовывает большую форму на каждое нажатие ради значений, которые ты читаешь только при отправке.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.