← React/Формы в React#265 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Формы в React

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

В React существуют два подхода к работе с формами.

Контролируемые инпуты (Controlled)

Значение инпута хранится в state и синхронизируется через value + onChange. React является "источником истины":

function ControlledInput() {
  const [name, setName] = useState('')

  return (
    <input
      value={name}                          // State -> DOM
      onChange={e => setName(e.target.value)} // DOM -> State
    />
  )
}

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

Неконтролируемые инпуты (Uncontrolled)

Значение хранится в DOM, доступ через useRef:

function UncontrolledInput() {
  const inputRef = useRef(null)

  function handleSubmit() {
    console.log(inputRef.current.value)  // читаем из DOM напрямую
  }

  return <input ref={inputRef} defaultValue="начальное" />
}

Используется реже: для интеграции с не-React библиотеками, файловых инпутов, когда performance критичен.

Паттерн: один onChange для всей формы

Вместо отдельного обработчика для каждого поля — один общий:

function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
  })

  // Один обработчик для всех полей!
  function handleChange(e) {
    const { name, value } = e.target
    setFormData(prev => ({
      ...prev,
      [name]: value  // вычисляемое имя свойства
    }))
  }

  return (
    <form>
      <input name="name"     value={formData.name}     onChange={handleChange} />
      <input name="email"    value={formData.email}    onChange={handleChange} />
      <input name="password" value={formData.password} onChange={handleChange} />
    </form>
  )
}

Ключ — атрибут name у инпута и вычисляемое свойство [name]: value.

Валидация формы

function RegistrationForm() {
  const [formData, setFormData] = useState({ name: '', email: '', password: '' })
  const [errors, setErrors] = useState({})

  function validate(data) {
    const errs = {}

    if (!data.name.trim()) {
      errs.name = 'Имя обязательно'
    }

    if (!data.email.trim()) {
      errs.email = 'Email обязателен'
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
      errs.email = 'Некорректный email'
    }

    if (!data.password) {
      errs.password = 'Пароль обязателен'
    } else if (data.password.length < 6) {
      errs.password = 'Пароль минимум 6 символов'
    }

    return errs
  }

  function handleSubmit(e) {
    e.preventDefault()
    const errs = validate(formData)

    if (Object.keys(errs).length > 0) {
      setErrors(errs)
      return
    }

    // Форма валидна — отправляем
    console.log('Отправка:', formData)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input name="name" value={formData.name} onChange={handleChange} />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>
      <button type="submit">Зарегистрироваться</button>
    </form>
  )
}

Специальные инпуты в React

textarea — в React самозакрывающийся с value:

<textarea value={text} onChange={e => setText(e.target.value)} />
// В HTML textarea использует содержимое: <textarea>текст</textarea>

select:

<select value={selected} onChange={e => setSelected(e.target.value)}>
  <option value="ru">Русский</option>
  <option value="en">English</option>
</select>

checkbox:

<input
  type="checkbox"
  checked={isChecked}          // не value, а checked!
  onChange={e => setChecked(e.target.checked)}
/>

TypeScript для форм

interface FormData {
  name: string
  email: string
  password: string
}

interface FormErrors {
  name?: string
  email?: string
  password?: string
}

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  const { name, value } = e.target
  setFormData(prev => ({ ...prev, [name]: value }))
}

Библиотеки для форм

В реальных проектах часто используют:

  • React Hook Form — минимальные re-renders, хорошая производительность
  • Formik — полнофункциональное решение с Yup-валидацией
  • Zod — валидация схем с TypeScript
  • Но понимание базового паттерна controlled inputs обязательно.

    Примеры

    Контролируемые поля, единый обработчик onChange и валидация формы регистрации

    // Реализуем логику формы регистрации на чистом JS
    // без DOM — только бизнес-логика, как она работает в React
    
    // ============================================================
    // Контролируемая форма — state как источник истины
    // ============================================================
    
    function createRegistrationForm() {
      // Аналог: const [formData, setFormData] = useState({...})
      let formData = { name: '', email: '', password: '', role: 'user' }
      let errors = {}
      let submitted = false
    
      // Единый обработчик для всех полей
      // Аналог React: (e) => setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }))
      function handleChange(fieldName, value) {
        // Вычисляемое имя свойства — тот же паттерн что в React
        formData = { ...formData, [fieldName]: value }
    
        // Валидация в реальном времени (если уже пытались отправить)
        if (submitted) {
          errors = validate(formData)
        }
    
        console.log(`[onChange] ${fieldName}: "${value}"`)
      }
    
      // ============================================================
      // Валидация
      // ============================================================
    
      function validate(data) {
        const errs = {}
    
        // Имя обязательно
        if (!data.name.trim()) {
          errs.name = 'Имя обязательно'
        } else if (data.name.trim().length < 2) {
          errs.name = 'Имя минимум 2 символа'
        }
    
        // Email: обязателен + формат
        if (!data.email.trim()) {
          errs.email = 'Email обязателен'
        } else if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(data.email)) {
          errs.email = 'Некорректный формат email'
        }
    
        // Пароль: обязателен + минимум 6 символов
        if (!data.password) {
          errs.password = 'Пароль обязателен'
        } else if (data.password.length < 6) {
          errs.password = `Пароль минимум 6 символов (сейчас ${data.password.length})`
        }
    
        return errs
      }
    
      function handleSubmit() {
        submitted = true
        errors = validate(formData)
    
        if (Object.keys(errors).length > 0) {
          console.log('[submit] ОШИБКИ ВАЛИДАЦИИ:', errors)
          return { success: false, errors }
        }
    
        console.log('[submit] Форма валидна! Данные:', {
          name: formData.name,
          email: formData.email,
          role: formData.role
          // password не логируем в реальном приложении!
        })
        return { success: true, data: formData }
      }
    
      return { handleChange, handleSubmit, getState: () => ({ formData: {...formData}, errors: {...errors} }) }
    }
    
    // ============================================================
    // Демонстрация работы формы
    // ============================================================
    
    const form = createRegistrationForm()
    
    console.log('=== Попытка отправить пустую форму ===')
    const result1 = form.handleSubmit()
    console.log('Ошибок:', Object.keys(result1.errors).length)  // 3
    
    console.log('
    === Заполняем поля ===')
    form.handleChange('name', 'Алексей')
    form.handleChange('email', 'не-email')  // некорректный
    form.handleChange('password', '123')    // слишком короткий
    
    console.log('
    === Снова пробуем отправить ===')
    const result2 = form.handleSubmit()
    console.log('Ошибка email:', result2.errors.email)
    console.log('Ошибка password:', result2.errors.password)
    
    console.log('
    === Исправляем ошибки ===')
    form.handleChange('email', 'alex@example.com')
    form.handleChange('password', 'secret123')
    
    console.log('
    === Финальная отправка ===')
    const result3 = form.handleSubmit()
    console.log('Успех:', result3.success)  // true

    Формы в React

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

    В React существуют два подхода к работе с формами.

    Контролируемые инпуты (Controlled)

    Значение инпута хранится в state и синхронизируется через value + onChange. React является "источником истины":

    function ControlledInput() {
      const [name, setName] = useState('')
    
      return (
        <input
          value={name}                          // State -> DOM
          onChange={e => setName(e.target.value)} // DOM -> State
        />
      )
    }

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

    Неконтролируемые инпуты (Uncontrolled)

    Значение хранится в DOM, доступ через useRef:

    function UncontrolledInput() {
      const inputRef = useRef(null)
    
      function handleSubmit() {
        console.log(inputRef.current.value)  // читаем из DOM напрямую
      }
    
      return <input ref={inputRef} defaultValue="начальное" />
    }

    Используется реже: для интеграции с не-React библиотеками, файловых инпутов, когда performance критичен.

    Паттерн: один onChange для всей формы

    Вместо отдельного обработчика для каждого поля — один общий:

    function RegistrationForm() {
      const [formData, setFormData] = useState({
        name: '',
        email: '',
        password: '',
      })
    
      // Один обработчик для всех полей!
      function handleChange(e) {
        const { name, value } = e.target
        setFormData(prev => ({
          ...prev,
          [name]: value  // вычисляемое имя свойства
        }))
      }
    
      return (
        <form>
          <input name="name"     value={formData.name}     onChange={handleChange} />
          <input name="email"    value={formData.email}    onChange={handleChange} />
          <input name="password" value={formData.password} onChange={handleChange} />
        </form>
      )
    }

    Ключ — атрибут name у инпута и вычисляемое свойство [name]: value.

    Валидация формы

    function RegistrationForm() {
      const [formData, setFormData] = useState({ name: '', email: '', password: '' })
      const [errors, setErrors] = useState({})
    
      function validate(data) {
        const errs = {}
    
        if (!data.name.trim()) {
          errs.name = 'Имя обязательно'
        }
    
        if (!data.email.trim()) {
          errs.email = 'Email обязателен'
        } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
          errs.email = 'Некорректный email'
        }
    
        if (!data.password) {
          errs.password = 'Пароль обязателен'
        } else if (data.password.length < 6) {
          errs.password = 'Пароль минимум 6 символов'
        }
    
        return errs
      }
    
      function handleSubmit(e) {
        e.preventDefault()
        const errs = validate(formData)
    
        if (Object.keys(errs).length > 0) {
          setErrors(errs)
          return
        }
    
        // Форма валидна — отправляем
        console.log('Отправка:', formData)
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <div>
            <input name="name" value={formData.name} onChange={handleChange} />
            {errors.name && <span className="error">{errors.name}</span>}
          </div>
          <button type="submit">Зарегистрироваться</button>
        </form>
      )
    }

    Специальные инпуты в React

    textarea — в React самозакрывающийся с value:

    <textarea value={text} onChange={e => setText(e.target.value)} />
    // В HTML textarea использует содержимое: <textarea>текст</textarea>

    select:

    <select value={selected} onChange={e => setSelected(e.target.value)}>
      <option value="ru">Русский</option>
      <option value="en">English</option>
    </select>

    checkbox:

    <input
      type="checkbox"
      checked={isChecked}          // не value, а checked!
      onChange={e => setChecked(e.target.checked)}
    />

    TypeScript для форм

    interface FormData {
      name: string
      email: string
      password: string
    }
    
    interface FormErrors {
      name?: string
      email?: string
      password?: string
    }
    
    function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
      const { name, value } = e.target
      setFormData(prev => ({ ...prev, [name]: value }))
    }

    Библиотеки для форм

    В реальных проектах часто используют:

  • React Hook Form — минимальные re-renders, хорошая производительность
  • Formik — полнофункциональное решение с Yup-валидацией
  • Zod — валидация схем с TypeScript
  • Но понимание базового паттерна controlled inputs обязательно.

    Примеры

    Контролируемые поля, единый обработчик onChange и валидация формы регистрации

    // Реализуем логику формы регистрации на чистом JS
    // без DOM — только бизнес-логика, как она работает в React
    
    // ============================================================
    // Контролируемая форма — state как источник истины
    // ============================================================
    
    function createRegistrationForm() {
      // Аналог: const [formData, setFormData] = useState({...})
      let formData = { name: '', email: '', password: '', role: 'user' }
      let errors = {}
      let submitted = false
    
      // Единый обработчик для всех полей
      // Аналог React: (e) => setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }))
      function handleChange(fieldName, value) {
        // Вычисляемое имя свойства — тот же паттерн что в React
        formData = { ...formData, [fieldName]: value }
    
        // Валидация в реальном времени (если уже пытались отправить)
        if (submitted) {
          errors = validate(formData)
        }
    
        console.log(`[onChange] ${fieldName}: "${value}"`)
      }
    
      // ============================================================
      // Валидация
      // ============================================================
    
      function validate(data) {
        const errs = {}
    
        // Имя обязательно
        if (!data.name.trim()) {
          errs.name = 'Имя обязательно'
        } else if (data.name.trim().length < 2) {
          errs.name = 'Имя минимум 2 символа'
        }
    
        // Email: обязателен + формат
        if (!data.email.trim()) {
          errs.email = 'Email обязателен'
        } else if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(data.email)) {
          errs.email = 'Некорректный формат email'
        }
    
        // Пароль: обязателен + минимум 6 символов
        if (!data.password) {
          errs.password = 'Пароль обязателен'
        } else if (data.password.length < 6) {
          errs.password = `Пароль минимум 6 символов (сейчас ${data.password.length})`
        }
    
        return errs
      }
    
      function handleSubmit() {
        submitted = true
        errors = validate(formData)
    
        if (Object.keys(errors).length > 0) {
          console.log('[submit] ОШИБКИ ВАЛИДАЦИИ:', errors)
          return { success: false, errors }
        }
    
        console.log('[submit] Форма валидна! Данные:', {
          name: formData.name,
          email: formData.email,
          role: formData.role
          // password не логируем в реальном приложении!
        })
        return { success: true, data: formData }
      }
    
      return { handleChange, handleSubmit, getState: () => ({ formData: {...formData}, errors: {...errors} }) }
    }
    
    // ============================================================
    // Демонстрация работы формы
    // ============================================================
    
    const form = createRegistrationForm()
    
    console.log('=== Попытка отправить пустую форму ===')
    const result1 = form.handleSubmit()
    console.log('Ошибок:', Object.keys(result1.errors).length)  // 3
    
    console.log('
    === Заполняем поля ===')
    form.handleChange('name', 'Алексей')
    form.handleChange('email', 'не-email')  // некорректный
    form.handleChange('password', '123')    // слишком короткий
    
    console.log('
    === Снова пробуем отправить ===')
    const result2 = form.handleSubmit()
    console.log('Ошибка email:', result2.errors.email)
    console.log('Ошибка password:', result2.errors.password)
    
    console.log('
    === Исправляем ошибки ===')
    form.handleChange('email', 'alex@example.com')
    form.handleChange('password', 'secret123')
    
    console.log('
    === Финальная отправка ===')
    const result3 = form.handleSubmit()
    console.log('Успех:', result3.success)  // true

    Задание

    Создай компонент App с формой обратной связи. Форма содержит поля "Имя" и "Сообщение" (textarea). Используй useState для хранения значений полей. При отправке формы (onSubmit) — проверяй что оба поля не пустые, если нет — показывай ошибку, если да — показывай сообщение об успехе и очищай форму.

    Подсказка

    e.preventDefault() предотвращает перезагрузку. Проверка: name.trim() === "" || message.trim() === "". onChange для textarea: e => setMessage(e.target.value). При успехе setSubmitted(true). Кнопка "Отправить ещё": setSubmitted(false).

    Загружаем среду выполнения...
    Загружаем AI-помощника...