$ sudo teach IT

Модуль 11: React + TypeScript

Урок 11.5: Формы и обработка событий

Продвинутый уровень 60 минут

1. Основы типизации форм

Формы — это одна из наиболее важных частей любого веб-приложения. При работе с React и TypeScript правильная типизация форм обеспечивает безопасность данных, улучшаетпользовательский опыт и помогает обнаруживать ошибки на ранних этапах разработки.

В этом уроке мы подробно разберём все аспекты работы с формами в React с TypeScript: от простых форм до сложных сценариев с валидацией, динамическими полями и управлением состоянием формы.

Типы событий в React

Перед началом работы с формами давайте разберём основные типы событий, которые используются в React:

React.FormEvent<HTMLFormElement> — событие отправки формы

React.ChangeEvent<HTMLInputElement> — событие изменения input

React.ChangeEvent<HTMLTextAreaElement> — событие изменения textarea

React.ChangeEvent<HTMLSelectElement> — событие изменения select

React.MouseEvent<HTMLButtonElement> — событие клика по кнопке

React.FocusEvent<HTMLInputElement> — событие фокуса

React.KeyboardEvent<HTMLInputElement> — событие нажатия клавиши

2. Типизация простых форм

Начнём с простых форм и постепенно перейдём к более сложным сценариям.

Форма входа

// Определяем интерфейс для данных формы
interface LoginFormData { email: string; password: string; rememberMe: boolean; } const LoginForm = () => { const [formData, setFormData] = useState<LoginFormData>({ email: '', password: '', rememberMe: false, }); const [errors, setErrors] = useState<Partial<Record<keyof LoginFormData, string>>>({}); // Типизированный обработчик изменения полей
const handleChange = ( e: React.ChangeEvent<HTMLInputElement> ) => { const { name, value, type, checked } = e.target; setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value, })); }; // Валидация формы
const validate = (): boolean => { const newErrors: typeof errors = {}; if (!formData.email) { newErrors.email = 'Email обязателен'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.email = 'Некорректный email'; } if (!formData.password) { newErrors.password = 'Пароль обязателен'; } else if (formData.password.length < 6) { newErrors.password = 'Пароль должен быть не менее 6 символов'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // Типизированный обработчик отправки формы
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (validate()) { console.log('Отправка формы:', formData); } }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <label htmlFor="password">Пароль</label> <input type="password" id="password" name="password" value={formData.password} onChange={handleChange} /> {errors.password && <span className="error">{errors.password}</span>} </div> <div> <input type="checkbox" id="rememberMe" name="rememberMe" checked={formData.rememberMe} onChange={handleChange} /> <label htmlFor="rememberMe">Запомнить меня</label> </div> <button type="submit">Войти</button> </form> ); };

3. Типизация формы регистрации

Форма регистрации — это более сложный пример, который включает множество полей и валидацию.

// Интерфейс для формы регистрации
interface RegistrationFormData { username: string; email: string; password: string; confirmPassword: string; gender: 'male' | 'female' | 'other'; birthDate: string; acceptTerms: boolean; } interface FormErrors { username?: string; email?: string; password?: string; confirmPassword?: string; gender?: string; birthDate?: string; acceptTerms?: string; } const RegistrationForm = () => { const [formData, setFormData] = useState<RegistrationFormData>({ username: '', email: '', password: '', confirmPassword: '', gender: 'other', birthDate: '', acceptTerms: false, }); const [errors, setErrors] = useState<FormErrors>({}); const [isSubmitting, setIsSubmitting] = useState(false); // Универсальный обработчик для всех типов полей
const handleChange = ( e: React.ChangeEvent< HTMLInputElement | HTMLSelectElement > ) => { const { name, value, type } = e.target; const checked = type === 'checkbox' ? (e.target as HTMLInputElement).checked : undefined; setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked! : value, })); }; // Комплексная валидация
const validate = (): FormErrors => { const newErrors: FormErrors = {}; if (!formData.username.trim()) { newErrors.username = 'Имя пользователя обязательно'; } else if (formData.username.length < 3) { newErrors.username = 'Имя должно быть не менее 3 символов'; } else if (formData.username.length > 20) { newErrors.username = 'Имя должно быть не более 20 символов'; } if (!formData.email) { newErrors.email = 'Email обязателен'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.email = 'Некорректный формат email'; } if (!formData.password) { newErrors.password = 'Пароль обязателен'; } else if (formData.password.length < 8) { newErrors.password = 'Пароль должен быть не менее 8 символов'; } else if (!/(?=.*[0-9])/.test(formData.password)) { newErrors.password = 'Пароль должен содержать хотя бы одну цифру'; } if (formData.password !== formData.confirmPassword) { newErrors.confirmPassword = 'Пароли не совпадают'; } if (!formData.birthDate) { newErrors.birthDate = 'Дата рождения обязательна'; } if (!formData.acceptTerms) { newErrors.acceptTerms = 'Необходимо принять условия'; } return newErrors; }; const handleSubmit = async ( e: React.FormEvent<HTMLFormElement> ) => { e.preventDefault(); const validationErrors = validate(); setErrors(validationErrors); if (Object.keys(validationErrors).length === 0) { setIsSubmitting(true); try { await registerUser(formData); alert('Регистрация успешна!'); } catch (err) { setErrors({ email: 'Ошибка регистрации' }); } finally { setIsSubmitting(false); } } }; return ( <form onSubmit={handleSubmit}> {/*. Поля формы с типизированными обработчиками */}
<button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Отправка...' : 'Зарегистрироваться'} </button> </form> ); };

4. Типизация формы с динамическими полями

Иногда количество полей формы определяется динамически. Давайте разберём, как типизировать такие формы.

// Форма с динамическим списком полей
interface DynamicFormState { title: string; fields: { id: string; label: string; type: 'text' | 'number' | 'email' | 'checkbox'; value: string | number | boolean; required: boolean; }[]; } const DynamicForm = () => { const [formState, setFormState] = useState<DynamicFormState>({ title: 'Моя форма', fields: [ { id: '1', label: 'Имя', type: 'text', value: '', required: true }, { id: '2', label: 'Возраст', type: 'number', value: 0, required: false }, ], }); // Обновление значения конкретного поля
const handleFieldChange = ( fieldId: string, value: string | number | boolean ) => { setFormState(prev => ({ ...prev, fields: prev.fields.map(field => field.id === fieldId ? { ...field, value } : field ), })); }; // Добавление нового поля
const addField = () => { const newField: DynamicFormState['fields'][0] = { id: Date.now().toString(), label: `Поле ${formState.fields.length + 1}`, type: 'text', value: '', required: false, }; setFormState(prev => ({ ...prev, fields: [...prev.fields, newField], })); }; // Удаление поля
const removeField = (fieldId: string) => { setFormState(prev => ({ ...prev, fields: prev.fields.filter(field => field.id !== fieldId), })); }; return ( <form> <input value={formState.title} onChange={(e) => setFormState(prev => ({ ...prev, title: e.target.value, }))} /> {formState.fields.map(field => ( <div key={field.id}> <label>{field.label}</label> <input type={field.type} value={field.value as string} onChange={(e) => { const newValue = field.type === 'number' ? Number(e.target.value) : field.type === 'checkbox' ? (e.target as HTMLInputElement).checked : e.target.value; handleFieldChange(field.id, newValue); }} required={field.required} /> <button type="button" onClick={() => removeField(field.id)}> Удалить </button> </div> ))} <button type="button" onClick={addField}>Добавить поле</button> </form> ); };

5. Типизация формы с вложенными объектами

Часто данные формы содержат вложенные объекты. Давайте разберём, как типизировать такие формы.

// Форма с вложенными объектами
interface Address { street: string; city: string; zipCode: string; country: string; } interface ContactFormState { personalInfo: { firstName: string; lastName: string; }; address: Address; preferences: { newsletter: boolean; notifications: boolean; }; } const ContactForm = () => { const [formData, setFormData] = useState<ContactFormState>({ personalInfo: { firstName: '', lastName: '' }, address: { street: '', city: '', zipCode: '', country: '' }, preferences: { newsletter: false, notifications: true }, }); // Типизированный обработчик для вложенных полей
const handleNestedChange = < K1 extends keyof ContactFormState, K2 extends keyof ContactFormState[K1] >( section: K1, field: K2, value: ContactFormState[K1][K2] ) => { setFormData(prev => ({ ...prev, [section]: { ...prev[section], [field]: value, }, })); }; return ( <form> <input value={formData.personalInfo.firstName} onChange={(e) => handleNestedChange('personalInfo', 'firstName', e.target.value) } /> {/*. TypeScript проверяет, что section и field являются валидными ключами */}
</form> ); };

6. Кастомные компоненты форм

Создание переиспользуемых компонентов форм — это важная часть разработки. Давайте создадим несколько типизированных компонентов.

// Типизированный компонент Input
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string; error?: string; helperText?: string; } const Input: React.FC<InputProps> = ({ label, error, helperText, id, ...props }) => { const inputId = id || label.toLowerCase().replace(/\s+/g, '-'); return ( <div className="input-group"> <label htmlFor={inputId}>{label}</label> <input id={inputId} className={`input ${error ? 'input-error' : ''}`} {...props} /> {error && <span className="error-message">{error}</span>} {helperText && !error && ( <span className="helper-text">{helperText}</span> )} </div> ); }; // Типизированный компонент Select
interface SelectOption<T extends string> { value: T; label: string; disabled?: boolean; } interface SelectProps<T extends string> { label: string; value: T; onChange: (value: T) => void; options: SelectOption<T>[]; error?: string; } function Select<T extends string>({ label, value, onChange, options, error, }: SelectProps<T>): React.ReactElement { return ( <div className="select-group"> <label>{label}</label> <select value={value} onChange={(e) => onChange(e.target.value as T)} > {options.map(option => ( <option key={option.value} value={option.value} disabled={option.disabled} > {option.label} </option> ))} </select> {error && <span className="error">{error}</span>} </div> ); } // Использование типизированного Select:
type Status = 'active' | 'inactive' | 'pending'; <Select<Status> label="Статус" value={status} onChange={setStatus} options={[ { value: 'active', label: 'Активный' }, { value: 'inactive', label: 'Неактивный' }, { value: 'pending', label: 'Ожидание' }, ]} />

7. Типизация формы с файлами

Работа с файлами в формах требует особого внимания к типизации. Давайте разберём, как типизировать формы с загрузкой файлов.

// Форма с загрузкой файлов
interface FileUploadState { files: File[]; previews: string[]; uploading: boolean; progress: number; } const FileUploadForm = () => { const [state, setState] = useState<FileUploadState>({ files: [], previews: [], uploading: false, progress: 0, }); // Типизированный обработчик выбора файлов
const handleFileSelect = ( e: React.ChangeEvent<HTMLInputElement> ) => { const selectedFiles = e.target.files; if (!selectedFiles) return; const filesArray = Array.from(selectedFiles); const previewsArray = filesArray.map(file => { const reader = new FileReader(); reader.readAsDataURL(file); return new Promise<string>(resolve => { reader.onload = () => resolve(reader.result as string); }); }); Promise.all(previewsArray).then(previews => { setState(prev => ({ ...prev, files: [...prev.files, ...filesArray], previews: [...prev.previews, ...previews], })); }); }; // Удаление файла
const removeFile = (index: number) => { setState(prev => ({ ...prev, files: prev.files.filter((_, i) => i !== index), previews: prev.previews.filter((_, i) => i !== index), })); }; return ( <div> <input type="file" multiple onChange={handleFileSelect} accept="image/*" /> {state.previews.map((preview, index) => ( <div key={index}> <img src={preview} alt={`Превью ${index}`} style={{ width: '100px' }} /> <button onClick={() => removeFile(index)}>Удалить</button> </div> ))} </div> ); };

8. Практическое задание

Выполните следующие задания для закрепления материала:

Задание 1: Форма профиля

  1. Создайте форму редактирования профиля с полями: имя, email, телефон, аватар
  2. Добавьте валидацию всех полей
  3. Реализуйте предпросмотр аватара при загрузке
  4. Добавьте кнопку сохранения с индикацией загрузки

Задание 2: Форма с шагами

  1. Создайте многошаговую форму (Wizard Form)
  2. Каждый шаг — отдельный компонент с валидацией
  3. Сохраняйте данные между шагами
  4. Добавьте навигацию (назад/вперёд)

Задание 3: Кастомные компоненты

  1. Создайте типизированный компонент DatePicker
  2. Создайте типизированный компонент MultiSelect сgenerics
  3. Создайте типизированный компонент FileUploader с превью
  4. Документируйте все пропсы с помощью JSDoc

9. Итоги урока

В этом уроке мы подробно изучили:

  • Типизацию событий форм (FormEvent, ChangeEvent)
  • Создание интерфейсов для данных формы
  • Валидацию форм с типизацией ошибок
  • Работу с динамическими полями формы
  • Типизацию вложенных объектов в формах
  • Создание переиспользуемых компонентов форм
  • Работу с файлами в формах

Советы и лучшие практики

  1. Всегда определяйте интерфейсы для данных формы
  2. Используйте Partial для типизации ошибок формы
  3. Создавайте переиспользуемые компоненты форм
  4. Добавляйте валидацию на клиенте и сервере
  5. Используйтеgenerics для создания гибких компонентов

В следующем уроке мы разберём типизацию API-запросов в React.

Отличная работа! Вы освоили типизацию форм в React!

Тест по типизации форм

1 вопрос

Тест по валидации форм

1 вопрос

Тест по компонентам форм

1 вопрос

Типизация событий формы

Premium