Події та форми
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 -- хук для побічних ефектів: завантаження даних, підписки, таймери та інше.
Корисні посилання:
- React.dev: Responding to Events -- події в React
- React.dev: Sharing State Between Components -- де зберігати state форми
- React Hook Form -- популярна бібліотека для форм (для майбутнього)