Урок 10

Оптимизация форм

Формы с большим количеством полей могут тормозить. Разберём приёмы оптимизации.

Проблема ререндеров

Каждый ввод символа вызывает setState, что перерисовывает весь компонент и его детей:

function Form() {
    const [values, setValues] = useState({ a: '', b: '', c: '' });

    function handleChange(e) {
        setValues((v) => ({ ...v, [e.target.name]: e.target.value }));
    }

    return (
        <>
            <input name="a" value={values.a} onChange={handleChange} />
            <input name="b" value={values.b} onChange={handleChange} />
            <input name="c" value={values.c} onChange={handleChange} />
        </>
    );
}

При вводе в поле a перерисовываются все три поля.

Решение 1: отдельные компоненты

Вынеси поля в компоненты и мемоизируй:

const Field = memo(function Field({ name, value, onChange }) {
    return <input name={name} value={value} onChange={onChange} />;
});

Решение 2: React Hook Form

React Hook Form использует неконтролируемые поля — ререндеров почти нет:

const { register, handleSubmit } = useForm();

<form onSubmit={handleSubmit(onSubmit)}>
    <input {...register('a')} />
    <input {...register('b')} />
    <input {...register('c')} />
</form>

Решение 3: useRef для полей

Если значение нужно только при отправке — не храни в стейте:

const refs = {
    a: useRef<HTMLInputElement>(null),
    b: useRef<HTMLInputElement>(null),
};

function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    console.log(refs.a.current?.value, refs.b.current?.value);
}

Мемоизация обработчиков

useCallback для обработчиков, передаваемых в мемоизированные дочерние компоненты:

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setValues((v) => ({ ...v, [e.target.name]: e.target.value }));
}, []);

Не создавай объекты в пропсах

// Плохо: новый объект при каждом рендере
<Input config={{ type: 'text' }} />

// Хорошо: константа
const config = { type: 'text' };
<Input config={config} />

Debounce для поиска

Если поле запускает поиск — используй debounce:

function useDebounce<T>(value: T, delay: number): T {
    const [debounced, setDebounced] = useState(value);

    useEffect(() => {
        const id = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(id);
    }, [value, delay]);

    return debounced;
}

const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);

useEffect(() => {
    if (debouncedQuery) search(debouncedQuery);
}, [debouncedQuery]);

Ленивая валидация

Не валидируй на каждый ввод, если это дорого. Валидируй при blur или отправке.

<input onBlur={validate} />

Виртуализация

Если полей сотни — используй виртуализацию (react-window, @tanstack/react-virtual). Рендерятся только видимые поля.

Разделение на шаги

Форма-мастер: каждый шаг — отдельный компонент. Меньше полей одновременно — меньше ререндеров.

function Wizard() {
    const [step, setStep] = useState(1);

    return (
        <>
            {step === 1 && <Step1 onNext={() => setStep(2)} />}
            {step === 2 && <Step2 onNext={() => setStep(3)} />}
        </>
    );
}

Мемоизация вычислений

Если в форме есть дорогие вычисления (например, валидация сложной схемы) — useMemo:

const isValid = useMemo(() => validate(values), [values]);

Итоги

  • Контролируемые поля ререндерят всё
  • React Hook Form уменьшает ререндеры
  • memo, useCallback, useMemo помогают
  • Debounce для поиска
  • Ленивая валидация
  • Виртуализация для больших форм
  • Форма-мастер — пошагово