Стандартный React-подход к формам — контролируемые компоненты: каждый ввод в поле вызывает setState → ре-рендер компонента. Для формы с 20 полями это 20 ре-рендеров при каждом нажатии клавиши:
// Контролируемая форма: рендер при КАЖДОМ нажатии клавиши
function SlowForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
// ... ещё 18 полей
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
{/* На каждый keydown: setState → рендер всех 20 полей */}
</form>
)
}React Hook Form использует неконтролируемые компоненты: читает значения из DOM через refs только при необходимости (submit или валидация). Результат — нулевые ре-рендеры при вводе.
import { useForm } from 'react-hook-form'
function RegistrationForm() {
const {
register, // регистрирует поля
handleSubmit, // обёртка для onSubmit
formState: { errors, isSubmitting, isValid },
watch, // отслеживает значения полей
setValue, // программная установка значения
reset, // сброс формы
} = useForm({
defaultValues: {
name: '',
email: '',
age: 18,
}
})
const onSubmit = async (data) => {
// data: { name, email, age } — только при submit!
await saveUser(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', {
required: 'Имя обязательно',
minLength: { value: 2, message: 'Минимум 2 символа' },
})}
/>
{errors.name && <p>{errors.name.message}</p>}
<input
{...register('email', {
required: 'Email обязателен',
pattern: { value: /^[^@]+@[^@]+$/, message: 'Неверный email' },
})}
/>
{errors.email && <p>{errors.email.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Сохраняем...' : 'Зарегистрироваться'}
</button>
</form>
)
}useForm({
mode: 'onSubmit', // по умолчанию: только при submit
mode: 'onBlur', // при потере фокуса
mode: 'onChange', // при каждом изменении (как раньше)
mode: 'onTouched', // первый раз onBlur, потом onChange
mode: 'all', // всегда
})import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
const schema = z.object({
name: z.string().min(2, 'Минимум 2 символа').max(50),
email: z.string().email('Неверный email'),
age: z.number().min(18, 'Должно быть 18+').max(120),
password: z.string().min(8, 'Минимум 8 символов'),
confirmPassword: z.string(),
}).refine(
data => data.password === data.confirmPassword,
{ message: 'Пароли не совпадают', path: ['confirmPassword'] }
)
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema), // Zod проводит валидацию
})
// TypeScript автоматически знает типы полей из schema!
}Некоторые компоненты (Select из Material UI, DatePicker) не работают с нативным ref. Используйте Controller:
import { Controller } from 'react-hook-form'
<Controller
name="country"
control={control}
rules={{ required: 'Выберите страну' }}
render={({ field }) => (
<CustomSelect
{...field} // value, onChange, onBlur
options={countries}
/>
)}
/>const { watch, setValue } = useForm()
// Отслеживаем значение поля (вызывает ре-рендер)
const country = watch('country')
const [city, country] = watch(['city', 'country'])
// Программная установка
function fillFromProfile() {
setValue('name', currentUser.name)
setValue('email', currentUser.email, { shouldValidate: true })
}
// Подписка без ре-рендера (для side effects):
useEffect(() => {
const subscription = watch((values) => {
console.log('Форма изменилась:', values)
})
return () => subscription.unsubscribe()
}, [watch])import { useFieldArray } from 'react-hook-form'
const { fields, append, remove } = useFieldArray({
control,
name: 'phoneNumbers',
})
return (
<div>
{fields.map((field, i) => (
<div key={field.id}>
<input {...register('phoneNumbers.' + i + '.number')} />
<button onClick={() => remove(i)}>Удалить</button>
</div>
))}
<button onClick={() => append({ number: '' })}>Добавить телефон</button>
</div>
)Реализация концепции react-hook-form: неконтролируемые поля с ref, подписочная система валидации, сбор данных только при submit
// Строим упрощённый аналог react-hook-form:
// неконтролируемые поля + валидация по событиям + нулевые рендеры.
// --- Движок валидации ---
function createValidator(rules) {
return function validate(value) {
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
const ruleValue = typeof ruleConfig === 'object' ? ruleConfig.value : ruleConfig
const message = typeof ruleConfig === 'object' ? ruleConfig.message : null
switch (ruleName) {
case 'required':
if (ruleValue && (!value || value.trim() === '')) {
return message || 'Поле обязательно'
}
break
case 'minLength':
if (value && value.length < ruleValue) {
return message || ('Минимум ' + ruleValue + ' символов')
}
break
case 'maxLength':
if (value && value.length > ruleValue) {
return message || ('Максимум ' + ruleValue + ' символов')
}
break
case 'pattern':
if (value && !ruleValue.test(value)) {
return message || 'Неверный формат'
}
break
case 'min':
if (value !== '' && Number(value) < ruleValue) {
return message || ('Минимальное значение: ' + ruleValue)
}
break
case 'max':
if (value !== '' && Number(value) > ruleValue) {
return message || ('Максимальное значение: ' + ruleValue)
}
break
}
}
return null // нет ошибки
}
}
// --- useForm аналог ---
function createForm(config = {}) {
const { defaultValues = {}, mode = 'onSubmit' } = config
// Храним значения полей (неконтролируемо — только для чтения)
const fieldValues = { ...defaultValues }
const fieldErrors = {}
const fieldValidators = {}
const touchedFields = new Set()
let renderCount = 0
function simulateRender(reason) {
renderCount++
console.log(' [Рендер #' + renderCount + '] причина:', reason)
}
function register(name, rules = {}) {
// Сохраняем валидатор для поля
fieldValidators[name] = createValidator(rules)
fieldValues[name] = fieldValues[name] ?? defaultValues[name] ?? ''
// Возвращаем "пропсы" для поля (как {...register('name')})
return {
name,
defaultValue: fieldValues[name],
// onChange: только записываем значение, БЕЗ ре-рендера
onChange(value) {
fieldValues[name] = value
if (mode === 'onChange' || (mode === 'onTouched' && touchedFields.has(name))) {
const error = fieldValidators[name]?.(value)
const hadError = !!fieldErrors[name]
fieldErrors[name] = error || undefined
// Рендер только если статус ошибки ИЗМЕНИЛСЯ
if (!!error !== hadError) {
simulateRender('ошибка изменилась для ' + name)
}
}
},
// onBlur: валидация при потере фокуса
onBlur() {
touchedFields.add(name)
if (mode === 'onBlur' || mode === 'onTouched' || mode === 'all') {
const error = fieldValidators[name]?.(fieldValues[name])
const hadError = !!fieldErrors[name]
fieldErrors[name] = error || undefined
if (!!error !== hadError) {
simulateRender('blur-валидация для ' + name)
}
}
}
}
}
function handleSubmit(onValid, onInvalid) {
return function submit() {
console.log('
Submit: валидация всех полей...')
let hasErrors = false
for (const [name, validate] of Object.entries(fieldValidators)) {
const error = validate(fieldValues[name])
fieldErrors[name] = error || undefined
if (error) {
hasErrors = true
console.log(' Ошибка в поле "' + name + '":', error)
}
}
// Один рендер для показа всех ошибок (если они есть)
if (hasErrors) {
simulateRender('показ ошибок submit')
console.log('Форма невалидна. Вызываем onInvalid')
onInvalid?.(fieldErrors)
return
}
console.log('Форма валидна! Данные:', JSON.stringify(fieldValues))
onValid(fieldValues)
}
}
function getFormState() {
const errorCount = Object.values(fieldErrors).filter(Boolean).length
return {
errors: { ...fieldErrors },
isValid: errorCount === 0,
isDirty: JSON.stringify(fieldValues) !== JSON.stringify(defaultValues),
renderCount,
}
}
return { register, handleSubmit, getFormState, getValues: () => ({ ...fieldValues }) }
}
// --- Тест 1: Нулевые рендеры при вводе ---
console.log('=== Тест 1: Рендеры при вводе (mode: onSubmit) ===')
const form1 = createForm({ defaultValues: { name: '', email: '' }, mode: 'onSubmit' })
const nameField = form1.register('name', { required: 'Имя обязательно' })
const emailField = form1.register('email', { required: true, pattern: { value: /S+@S+/, message: 'Неверный email' } })
// Симулируем ввод — РЕНДЕРОВ НЕТ!
nameField.onChange('А')
nameField.onChange('Ал')
nameField.onChange('Але')
nameField.onChange('Алек')
nameField.onChange('Алексей')
emailField.onChange('alex@example.com')
console.log('Рендеров во время ввода:', form1.getFormState().renderCount) // 0!
// --- Тест 2: Submit ---
console.log('
=== Тест 2: Submit с валидными данными ===')
const onValid = (data) => console.log('Успех! Данные:', JSON.stringify(data))
form1.handleSubmit(onValid)()
console.log('Рендеров после submit:', form1.getFormState().renderCount) // 0 (нет ошибок)
// --- Тест 3: Submit с ошибками ---
console.log('
=== Тест 3: Submit с ошибками ===')
const form2 = createForm({ defaultValues: { name: '', age: 0 }, mode: 'onSubmit' })
form2.register('name', { required: 'Имя обязательно', minLength: { value: 2, message: 'Мин. 2 символа' } })
form2.register('age', { required: true, min: { value: 18, message: 'Должно быть 18+' } })
form2.handleSubmit(
() => console.log('Успех'),
(errors) => console.log('Ошибки:', JSON.stringify(errors))
)()
// --- Тест 4: mode: onBlur ---
console.log('
=== Тест 4: Режим onBlur ===')
const form3 = createForm({ defaultValues: { email: '' }, mode: 'onBlur' })
const emailField3 = form3.register('email', { pattern: { value: /S+@S+/, message: 'Неверный email' } })
emailField3.onChange('notanemail') // нет рендера
console.log('Рендеров после onChange:', form3.getFormState().renderCount) // 0
emailField3.onBlur() // теперь валидация
console.log('Рендеров после onBlur:', form3.getFormState().renderCount) // 1 (ошибка!)
console.log('Ошибка email:', form3.getFormState().errors.email)Стандартный React-подход к формам — контролируемые компоненты: каждый ввод в поле вызывает setState → ре-рендер компонента. Для формы с 20 полями это 20 ре-рендеров при каждом нажатии клавиши:
// Контролируемая форма: рендер при КАЖДОМ нажатии клавиши
function SlowForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
// ... ещё 18 полей
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
{/* На каждый keydown: setState → рендер всех 20 полей */}
</form>
)
}React Hook Form использует неконтролируемые компоненты: читает значения из DOM через refs только при необходимости (submit или валидация). Результат — нулевые ре-рендеры при вводе.
import { useForm } from 'react-hook-form'
function RegistrationForm() {
const {
register, // регистрирует поля
handleSubmit, // обёртка для onSubmit
formState: { errors, isSubmitting, isValid },
watch, // отслеживает значения полей
setValue, // программная установка значения
reset, // сброс формы
} = useForm({
defaultValues: {
name: '',
email: '',
age: 18,
}
})
const onSubmit = async (data) => {
// data: { name, email, age } — только при submit!
await saveUser(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', {
required: 'Имя обязательно',
minLength: { value: 2, message: 'Минимум 2 символа' },
})}
/>
{errors.name && <p>{errors.name.message}</p>}
<input
{...register('email', {
required: 'Email обязателен',
pattern: { value: /^[^@]+@[^@]+$/, message: 'Неверный email' },
})}
/>
{errors.email && <p>{errors.email.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Сохраняем...' : 'Зарегистрироваться'}
</button>
</form>
)
}useForm({
mode: 'onSubmit', // по умолчанию: только при submit
mode: 'onBlur', // при потере фокуса
mode: 'onChange', // при каждом изменении (как раньше)
mode: 'onTouched', // первый раз onBlur, потом onChange
mode: 'all', // всегда
})import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
const schema = z.object({
name: z.string().min(2, 'Минимум 2 символа').max(50),
email: z.string().email('Неверный email'),
age: z.number().min(18, 'Должно быть 18+').max(120),
password: z.string().min(8, 'Минимум 8 символов'),
confirmPassword: z.string(),
}).refine(
data => data.password === data.confirmPassword,
{ message: 'Пароли не совпадают', path: ['confirmPassword'] }
)
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema), // Zod проводит валидацию
})
// TypeScript автоматически знает типы полей из schema!
}Некоторые компоненты (Select из Material UI, DatePicker) не работают с нативным ref. Используйте Controller:
import { Controller } from 'react-hook-form'
<Controller
name="country"
control={control}
rules={{ required: 'Выберите страну' }}
render={({ field }) => (
<CustomSelect
{...field} // value, onChange, onBlur
options={countries}
/>
)}
/>const { watch, setValue } = useForm()
// Отслеживаем значение поля (вызывает ре-рендер)
const country = watch('country')
const [city, country] = watch(['city', 'country'])
// Программная установка
function fillFromProfile() {
setValue('name', currentUser.name)
setValue('email', currentUser.email, { shouldValidate: true })
}
// Подписка без ре-рендера (для side effects):
useEffect(() => {
const subscription = watch((values) => {
console.log('Форма изменилась:', values)
})
return () => subscription.unsubscribe()
}, [watch])import { useFieldArray } from 'react-hook-form'
const { fields, append, remove } = useFieldArray({
control,
name: 'phoneNumbers',
})
return (
<div>
{fields.map((field, i) => (
<div key={field.id}>
<input {...register('phoneNumbers.' + i + '.number')} />
<button onClick={() => remove(i)}>Удалить</button>
</div>
))}
<button onClick={() => append({ number: '' })}>Добавить телефон</button>
</div>
)Реализация концепции react-hook-form: неконтролируемые поля с ref, подписочная система валидации, сбор данных только при submit
// Строим упрощённый аналог react-hook-form:
// неконтролируемые поля + валидация по событиям + нулевые рендеры.
// --- Движок валидации ---
function createValidator(rules) {
return function validate(value) {
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
const ruleValue = typeof ruleConfig === 'object' ? ruleConfig.value : ruleConfig
const message = typeof ruleConfig === 'object' ? ruleConfig.message : null
switch (ruleName) {
case 'required':
if (ruleValue && (!value || value.trim() === '')) {
return message || 'Поле обязательно'
}
break
case 'minLength':
if (value && value.length < ruleValue) {
return message || ('Минимум ' + ruleValue + ' символов')
}
break
case 'maxLength':
if (value && value.length > ruleValue) {
return message || ('Максимум ' + ruleValue + ' символов')
}
break
case 'pattern':
if (value && !ruleValue.test(value)) {
return message || 'Неверный формат'
}
break
case 'min':
if (value !== '' && Number(value) < ruleValue) {
return message || ('Минимальное значение: ' + ruleValue)
}
break
case 'max':
if (value !== '' && Number(value) > ruleValue) {
return message || ('Максимальное значение: ' + ruleValue)
}
break
}
}
return null // нет ошибки
}
}
// --- useForm аналог ---
function createForm(config = {}) {
const { defaultValues = {}, mode = 'onSubmit' } = config
// Храним значения полей (неконтролируемо — только для чтения)
const fieldValues = { ...defaultValues }
const fieldErrors = {}
const fieldValidators = {}
const touchedFields = new Set()
let renderCount = 0
function simulateRender(reason) {
renderCount++
console.log(' [Рендер #' + renderCount + '] причина:', reason)
}
function register(name, rules = {}) {
// Сохраняем валидатор для поля
fieldValidators[name] = createValidator(rules)
fieldValues[name] = fieldValues[name] ?? defaultValues[name] ?? ''
// Возвращаем "пропсы" для поля (как {...register('name')})
return {
name,
defaultValue: fieldValues[name],
// onChange: только записываем значение, БЕЗ ре-рендера
onChange(value) {
fieldValues[name] = value
if (mode === 'onChange' || (mode === 'onTouched' && touchedFields.has(name))) {
const error = fieldValidators[name]?.(value)
const hadError = !!fieldErrors[name]
fieldErrors[name] = error || undefined
// Рендер только если статус ошибки ИЗМЕНИЛСЯ
if (!!error !== hadError) {
simulateRender('ошибка изменилась для ' + name)
}
}
},
// onBlur: валидация при потере фокуса
onBlur() {
touchedFields.add(name)
if (mode === 'onBlur' || mode === 'onTouched' || mode === 'all') {
const error = fieldValidators[name]?.(fieldValues[name])
const hadError = !!fieldErrors[name]
fieldErrors[name] = error || undefined
if (!!error !== hadError) {
simulateRender('blur-валидация для ' + name)
}
}
}
}
}
function handleSubmit(onValid, onInvalid) {
return function submit() {
console.log('
Submit: валидация всех полей...')
let hasErrors = false
for (const [name, validate] of Object.entries(fieldValidators)) {
const error = validate(fieldValues[name])
fieldErrors[name] = error || undefined
if (error) {
hasErrors = true
console.log(' Ошибка в поле "' + name + '":', error)
}
}
// Один рендер для показа всех ошибок (если они есть)
if (hasErrors) {
simulateRender('показ ошибок submit')
console.log('Форма невалидна. Вызываем onInvalid')
onInvalid?.(fieldErrors)
return
}
console.log('Форма валидна! Данные:', JSON.stringify(fieldValues))
onValid(fieldValues)
}
}
function getFormState() {
const errorCount = Object.values(fieldErrors).filter(Boolean).length
return {
errors: { ...fieldErrors },
isValid: errorCount === 0,
isDirty: JSON.stringify(fieldValues) !== JSON.stringify(defaultValues),
renderCount,
}
}
return { register, handleSubmit, getFormState, getValues: () => ({ ...fieldValues }) }
}
// --- Тест 1: Нулевые рендеры при вводе ---
console.log('=== Тест 1: Рендеры при вводе (mode: onSubmit) ===')
const form1 = createForm({ defaultValues: { name: '', email: '' }, mode: 'onSubmit' })
const nameField = form1.register('name', { required: 'Имя обязательно' })
const emailField = form1.register('email', { required: true, pattern: { value: /S+@S+/, message: 'Неверный email' } })
// Симулируем ввод — РЕНДЕРОВ НЕТ!
nameField.onChange('А')
nameField.onChange('Ал')
nameField.onChange('Але')
nameField.onChange('Алек')
nameField.onChange('Алексей')
emailField.onChange('alex@example.com')
console.log('Рендеров во время ввода:', form1.getFormState().renderCount) // 0!
// --- Тест 2: Submit ---
console.log('
=== Тест 2: Submit с валидными данными ===')
const onValid = (data) => console.log('Успех! Данные:', JSON.stringify(data))
form1.handleSubmit(onValid)()
console.log('Рендеров после submit:', form1.getFormState().renderCount) // 0 (нет ошибок)
// --- Тест 3: Submit с ошибками ---
console.log('
=== Тест 3: Submit с ошибками ===')
const form2 = createForm({ defaultValues: { name: '', age: 0 }, mode: 'onSubmit' })
form2.register('name', { required: 'Имя обязательно', minLength: { value: 2, message: 'Мин. 2 символа' } })
form2.register('age', { required: true, min: { value: 18, message: 'Должно быть 18+' } })
form2.handleSubmit(
() => console.log('Успех'),
(errors) => console.log('Ошибки:', JSON.stringify(errors))
)()
// --- Тест 4: mode: onBlur ---
console.log('
=== Тест 4: Режим onBlur ===')
const form3 = createForm({ defaultValues: { email: '' }, mode: 'onBlur' })
const emailField3 = form3.register('email', { pattern: { value: /S+@S+/, message: 'Неверный email' } })
emailField3.onChange('notanemail') // нет рендера
console.log('Рендеров после onChange:', form3.getFormState().renderCount) // 0
emailField3.onBlur() // теперь валидация
console.log('Рендеров после onBlur:', form3.getFormState().renderCount) // 1 (ошибка!)
console.log('Ошибка email:', form3.getFormState().errors.email)Создай форму регистрации с валидацией в стиле react-hook-form. Компонент должен: использовать неконтролируемые поля через useRef; показывать ошибки только после submit; при успешной валидации вызывать onSubmit с данными формы. Заполни пропуски (???) для: получения значений из рефов, валидации email по паттерну, проверки совпадения паролей.
Для получения значения из рефа: nameRef.current.value. Для проверки email: !emailPattern.test(email). Для сравнения паролей: password !== confirm.