Контролируемые и неконтролируемые компоненты

Контролируемый компонент (controlled) хранит значение в React state и управляет им через обработчик; неконтролируемый (uncontrolled) — хранит значение в DOM, React читает его через ref.

Зачем нужно

Различие между двумя подходами к формам — один из первых вопросов собеседований. Controlled компоненты — стандарт React-форм: значение всегда в state, валидация в реальном времени, условное отображение. Uncontrolled проще для простых случаев (файловый инпут, интеграция со сторонними библиотеками). Смешение подходов без понимания — источник распространённых багов.

Где используется

  • Controlled: формы с валидацией, зависимые поля (выбор города зависит от страны), форматирование при вводе
  • Uncontrolled: файловые инпуты (<input type="file">), интеграция с jQuery/сторонними библиотеками, простые формы без валидации
  • react-hook-form — использует uncontrolled + ref для лучшей производительности (нет ре-рендера при каждом нажатии)

Controlled Component

import { useState } from 'react';

function ControlledForm() {
  // Значение хранится в React state
  const [form, setForm] = useState({
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState({});

  const handleChange = (field) => (e) => {
    const value = e.target.value;
    setForm(prev => ({ ...prev, [field]: value }));

    // Валидация в реальном времени — возможна только в controlled
    if (field === 'email' && value && !value.includes('@')) {
      setErrors(prev => ({ ...prev, email: 'Некорректный email' }));
    } else {
      setErrors(prev => ({ ...prev, [field]: null }));
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Отправка:', form); // актуальные данные доступны в state
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        {/* value + onChange = controlled input */}
        <input
          type="email"
          value={form.email}       // значение из state
          onChange={handleChange('email')} // обновляем state
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input
          type="password"
          value={form.password}
          onChange={handleChange('password')}
          placeholder="Пароль"
        />
      </div>
      <button type="submit">Войти</button>
    </form>
  );
}

Uncontrolled Component

import { useRef } from 'react';

function UncontrolledForm() {
  // Значения живут в DOM, React только читает их через ref
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    // Читаем значения напрямую из DOM элементов
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    console.log('Отправка:', { email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* ref — ссылка на DOM элемент, нет value/onChange */}
      <input type="email" ref={emailRef} defaultValue="" placeholder="Email" />
      <input type="password" ref={passwordRef} placeholder="Пароль" />
      <button type="submit">Войти</button>
    </form>
  );
}

// Файловый инпут — всегда uncontrolled
function FileUpload() {
  const fileRef = useRef(null);

  const handleUpload = () => {
    const file = fileRef.current.files[0];
    console.log('Файл:', file.name, file.size);
  };

  return (
    <div>
      {/* value нельзя установить программно для type="file" */}
      <input type="file" ref={fileRef} />
      <button onClick={handleUpload}>Загрузить</button>
    </div>
  );
}

Сравнение

Controlled Uncontrolled
Источник истины React state DOM
Ре-рендер при вводе Да Нет
Валидация в реальном времени Легко Сложно
Доступ к значению state.value ref.current.value
Файловый инпут Нельзя Да

Частые ошибки

  • Переход controlled → uncontrolled — если value меняется с undefined на строку, React выбрасывает предупреждение; всегда инициализируйте state строкой.
  • Нет onChange при value — input с value без onChange становится read-only; React предупредит об этом.
  • Использование defaultValue с controlleddefaultValue для uncontrolled, value для controlled; не смешивайте.

Связанные темы

Ресурсы