Вивчай

Події та форми

React події та форми — рецепція готелю з дзвінком на стійці та людиною що заповнює формуReact події та форми — рецепція готелю з дзвінком на стійці та людиною що заповнює форму

У попередніх уроках ми вже використовували onClick та onChange. Тепер розберемо обробку подій у React детально, а також навчимось працювати з формами -- controlled components, валідація та типізація.


Події в React

Основний синтаксис

В React події пишуться у форматі camelCase та приймають функцію (не рядок, як у HTML):

// HTML:
// <button onclick="handleClick()">Натисни</button>

// React:
<button onClick={handleClick}>Натисни</button>
Увага

onClick={handleClick} -- передаємо посилання на функцію. onClick={handleClick()} -- викликаємо функцію одразу (помилка!). Якщо потрібно передати аргумент, використовуй стрілкову функцію: onClick={() => handleClick(id)}.

Поширені події

function EventExamples() {
  return (
    <div>
      {/* Мишка */}
      <button onClick={() => console.log('Клік!')}>Клік</button>
      <div onMouseEnter={() => console.log('Навів')}>Наведи</div>
      <div onMouseLeave={() => console.log('Забрав')}>Забери</div>

      {/* Клавіатура */}
      <input onKeyDown={(e) => console.log('Натиснув:', e.key)} />

      {/* Фокус */}
      <input onFocus={() => console.log('Фокус')} />
      <input onBlur={() => console.log('Втратив фокус')} />

      {/* Форма */}
      <form onSubmit={(e) => { e.preventDefault(); console.log('Submit!') }}>
        <input onChange={(e) => console.log(e.target.value)} />
        <button type="submit">Надіслати</button>
      </form>
    </div>
  )
}

Event object та типізація

React обгортає браузерні події в SyntheticEvent -- кросбраузерну обгортку. У TypeScript кожна подія має свій тип:

function TypedEvents() {
  // Клік по кнопці
  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log('Координати:', e.clientX, e.clientY)
  }

  // Зміна input
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    console.log('Значення:', e.target.value)
  }

  // Відправка форми
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    console.log('Форма відправлена')
  }

  // Натискання клавіші
  function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') {
      console.log('Enter натиснутий!')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <button onClick={handleClick}>Надіслати</button>
    </form>
  )
}
Порада

Не треба запам'ятовувати всі типи подій. Пиши обробник inline onChange={(e) => ...} -- TypeScript автоматично виведе тип e. Потім, якщо винесеш функцію окремо, IDE підкаже потрібний тип.


Controlled Components

Controlled component -- це елемент форми, значення якого контролюється React-state:

import { useState } from 'react'

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

  return (
    <div>
      <input
        value={name}                              // значення з state
        onChange={(e) => setName(e.target.value)}  // оновлення state
      />
      <p>Ви ввели: {name}</p>
    </div>
  )
}

Схема потоку даних:

Користувач вводить → onChange → setState → React перемальовує → input показує нове value

Кожна зміна проходить через state. Це дає повний контроль: можна фільтрувати, трансформувати, валідувати вхідні дані.

Controlled vs Uncontrolled

// Controlled -- React контролює значення
<input value={name} onChange={(e) => setName(e.target.value)} />

// Uncontrolled -- DOM контролює значення
<input ref={inputRef} />

У більшості випадків використовуй controlled -- це стандартний підхід у React.


Форма з кількома полями

Окремий useState для кожного поля

function SimpleForm() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [message, setMessage] = useState('')

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    console.log({ name, email, message })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      <button type="submit">Надіслати</button>
    </form>
  )
}

Один useState для всієї форми

Для форм з багатьма полями зручніше зберігати все в одному об'єкті:

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

function RegistrationForm() {
  const [form, setForm] = useState<FormData>({
    name: '',
    email: '',
    password: '',
    role: 'user',
  })

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

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    console.log('Дані форми:', form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Ім'я:
        <input name="name" value={form.name} onChange={handleChange} />
      </label>

      <label>
        Email:
        <input name="email" type="email" value={form.email} onChange={handleChange} />
      </label>

      <label>
        Пароль:
        <input name="password" type="password" value={form.password} onChange={handleChange} />
      </label>

      <label>
        Роль:
        <select name="role" value={form.role} onChange={handleChange}>
          <option value="user">Користувач</option>
          <option value="admin">Адміністратор</option>
        </select>
      </label>

      <button type="submit">Зареєструватись</button>
    </form>
  )
}
Інфо

Зверни увагу на [name]: value -- це computed property name з ES6. Ключ об'єкта береться зі змінної name (яка дорівнює атрибуту name елемента форми). Один обробник для всіх полів!


Валідація форм

Базова валідація

import { useState } from 'react'

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

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

function ValidatedForm() {
  const [form, setForm] = useState<FormData>({
    name: '',
    email: '',
    password: '',
  })
  const [errors, setErrors] = useState<FormErrors>({})

  function validate(): FormErrors {
    const newErrors: FormErrors = {}

    if (form.name.trim().length < 2) {
      newErrors.name = "Ім'я має бути мінімум 2 символи"
    }

    if (!form.email.includes('@')) {
      newErrors.email = 'Невалідний email'
    }

    if (form.password.length < 6) {
      newErrors.password = 'Пароль мінімум 6 символів'
    }

    return newErrors
  }

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

    // Прибираємо помилку при зміні поля
    if (errors[name as keyof FormErrors]) {
      setErrors(prev => ({ ...prev, [name]: undefined }))
    }
  }

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    const newErrors = validate()

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

    console.log('Форма валідна! Дані:', form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="name"
          value={form.name}
          onChange={handleChange}
          placeholder="Ім'я"
        />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>

      <div>
        <input
          name="email"
          type="email"
          value={form.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <input
          name="password"
          type="password"
          value={form.password}
          onChange={handleChange}
          placeholder="Пароль"
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <button type="submit">Зареєструватись</button>
    </form>
  )
}

export default ValidatedForm

preventDefault -- не забудь!

Форма за замовчуванням перезавантажує сторінку при submit. У React завжди викликай e.preventDefault():

function handleSubmit(e: React.FormEvent) {
  e.preventDefault() // Зупиняємо перезавантаження!
  // ... обробка даних
}

Підсумок

  • Події в React -- camelCase (onClick, onChange, onSubmit)
  • Передавай функцію, а не її виклик: onClick={handleClick}
  • SyntheticEvent -- кросбраузерна обгортка. TypeScript типи: React.MouseEvent, React.ChangeEvent, тощо
  • Controlled components -- значення в state, оновлення через onChange
  • Один обробник для кількох полів: [e.target.name]: e.target.value
  • Валідація: окремий state для помилок, функція validate()
  • Завжди e.preventDefault() в onSubmit

Що далі?

Ми навчились працювати з подіями та формами. У наступному уроці розберемо useEffect -- хук для побічних ефектів: завантаження даних, підписки, таймери та інше.

Інфо

Корисні посилання: