React: Hooks
Когда использовать useMemo и useCallback?
Оба хука кэшируют результат между рендерами, пока не изменятся зависимости. Сами по себе они не ускоряют приложение «вслепую» — иногда добавляют лишнюю сложность.
| Хук | Что кэширует | Типичные случаи |
|---|---|---|
useMemo | Значение (результат вычисления) | Дорогой расчёт от props/state; стабильная ссылка на объект/массив, передаваемый в мемоизированный дочерний компонент |
useCallback | Функцию (стабильная ссылка) | Функция уходит в пропсы React.memo-ребёнка или в зависимости useEffect / других хуков |
Когда не злоупотреблять:
- Примитивы и дешёвые вычисления мемоизировать не нужно.
- Если дочерний компонент не мемоизирован,
useCallbackчасто не даёт выигрыша. - Профилировщик важнее догадок: сначала измерить, потом мемоизировать узкое место.
// useMemo — тяжёлое вычисление
const visible = useMemo(() => items.filter(expensivePredicate), [items]);
// useCallback — стабильный обработчик для memo-ребёнка
const onSelect = useCallback((id) => dispatch(select(id)), [dispatch]);
<ListItem memo onSelect={onSelect} />Батчинг в setState
Идея: несколько обновлений состояния группируются в один проход ре-рендера (один commit), чтобы не перерисовывать UI после каждого вызова.
| Контекст | Поведение |
|---|---|
| React 18+ | Автоматический батчинг в обработчиках событий, setTimeout, промисах, нативных подписках и т.д. (внутри корня React 18) |
| Ранее (17) | Батчинг в основном в синхронных обработчиках React-событий; в асинхронных колбэках — часто отдельные рендеры на каждый setState |
Функциональные обновления — когда новое состояние зависит от предыдущего:
setCount((c) => c + 1);
setCount((c) => c + 1);
// После батча count увеличится на 2, а не на 1Принудительно синхронный флаш (редко): flushSync из react-dom — обходит батчинг; использовать только при крайней необходимости.
Для собеса: батчинг уменьшает число рендеров; в React 18 батчинг по умолчанию шире; внутри одного события несколько
setStateобычно приводят к одному ре-рендеру.
Какие основные хуки используются в работе?
useState— хранит и обновляет состояние компонентаuseEffect— выполняет побочные эффекты (запросы, подписки, таймеры)useRef— хранит изменяемое значение без ре-рендера; даёт доступ к DOMuseCallback— мемоизирует функции, чтобы избежать лишних ре-рендеровuseMemo— мемоизирует вычисляемые значенияuseContext— читает значение из Context без дополнительных пропсовuseLayoutEffect— какuseEffect, но срабатывает синхронно после обновления DOM (до отрисовки браузером)forwardRef— не хук, а обёртка (HOC): передаётrefчерез компонент вниз. (В React 19refможно передавать как обычный проп,forwardRefбольше не нужен.)
Как useEffect заменяет методы жизненного цикла классов?
| Класс | Хуки |
|---|---|
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate | useEffect(() => { ... }, [deps]) — срабатывает и после первого рендера, если deps заданы; для «только обновления» вручную сравнивают с ref предыдущих значений |
componentWillUnmount | return () => { ... } из useEffect |
componentDidMount + componentWillUnmount | один useEffect с пустым массивом и cleanup |
Нюансы по сравнению с классами:
- Один
useEffectна одну «задачу» (подписка, запрос, таймер) — проще сочетать несколько эффектов, чем пихать всё вcomponentDidMount. - Эффект после отрисовки (асинхронно относительно браузера), не блокирует paint.
- При смене зависимостей: сначала cleanup старого эффекта, затем новый эффект.
В чём разница между useEffect и useLayoutEffect?
useEffect | useLayoutEffect | |
|---|---|---|
| Когда выполняется | После того, как браузер уже отрисовал изменения (асинхронно, не блокирует paint) | Синхронно после мутаций DOM, до paint |
| Блокирует визуализацию | Нет | Да — злоупотребление даёт лаги |
| Типичное использование | Запросы, подписки, логирование, большинство побочных эффектов | Измерение DOM (высота, скролл), синхронная корректировка DOM/стилей до показа кадра |
| SSR | Обычно безопасен | На сервере предупреждение / нужен typeof window — нет DOM |
Правило: по умолчанию
useEffect;useLayoutEffect— когда без синхронного чтения/записи DOM пользователь увидит «мигание» или неверную вёрстку.
Как работает useEffect — порядок выполнения
useEffect — хук для выполнения побочных эффектов (side effects) после рендера компонента.
useEffect(() => {
// эффект: запрос, подписка, DOM-манипуляция
return () => {
// cleanup: отписка, очистка таймеров
};
}, [dependency1, dependency2]); // массив зависимостейКогда запускается:
| Массив зависимостей | Когда выполняется |
|---|---|
| Не передан | После каждого рендера |
[] | Только после первого рендера (аналог componentDidMount) |
[dep] | После первого рендера + когда dep изменился |
Порядок работы:
- React рендерит компонент → обновляет DOM
- Браузер отображает изменения
- React запускает
useEffect(асинхронно, после paint) - При следующем рендере сначала выполняется cleanup предыдущего эффекта
Особенности хука useEffect
useEffect заменяет методы жизненного цикла классовых компонентов.
componentDidMount → useEffect(() => {}, [])
componentDidUpdate → useEffect(() => {}, [dep])
componentWillUnmount → return () => cleanup()
Принимает 2 аргумента:
useEffect(
() => { /* логика */ }, // 1. Функция с эффектом
[dep1, dep2] // 2. Массив зависимостей
);Особые случаи:
// Пустой массив — выполнить только при монтировании
useEffect(() => {
fetchData();
}, []);
// Без массива — выполнять при каждом рендере ⚠️
useEffect(() => {
console.log('every render');
});
// С зависимостями — выполнять при изменении dep
useEffect(() => {
updateTitle(title);
}, [title]);Как работает очистка в useEffect?
При изменении зависимостей React сначала выполняет функцию из return (очистку), затем запускает новый эффект.
useEffect(() => {
const timer = setTimeout(() => {}, 1000);
return () => clearTimeout(timer); // Очистка перед новым запуском
}, [dependency]);Порядок при изменении зависимости:
1. dependency изменилась
2. Срабатывает очистка предыдущего эффекта (return-функция)
3. Запускается новый эффект
Очистка выполняется перед каждым новым эффектом, а не только при размонтировании компонента.
Типичные случаи очистки:
// Подписка
useEffect(() => {
const sub = socket.subscribe(handler);
return () => sub.unsubscribe();
}, []);
// AbortController для fetch
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal }).then(setData);
return () => controller.abort();
}, [url]);Когда useEffect не нужен: derived state
Производное состояние (derived state) — значение, которое полностью вычисляется из существующего state или props. Его не нужно хранить в useState и синхронизировать через useEffect.
// ❌ Анти-паттерн: лишний state + useEffect → двойной рендер
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
setTotalPrice(quantity * PRICE_PER_ITEM);
}, [quantity]);
// ✅ Правильно: просто вычислить при рендере
const totalPrice = quantity * PRICE_PER_ITEM;
// ✅ Если вычисление тяжёлое — useMemo (без useEffect)
const sorted = useMemo(() => [...bigArray].sort(compareFn), [bigArray]);Правило: спроси себя — «это значение зависит от чего-то вне React?»
| Ситуация | Решение |
|---|---|
| Считается из state/props | Переменная при рендере |
| Тяжёлое вычисление из state/props | useMemo |
| Нужен fetch / подписка / таймер | useEffect |
| Нужно изменить DOM / сторонняя либа | useEffect |
Что такое ref и зачем нужен useRef?
ref — ссылка, которая сохраняется между рендерами, но при изменении не вызывает ре-рендер.
Два сценария использования:
// 1. Доступ к DOM-элементу
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // фокус на инпут после монтирования
}, []);
return <input ref={inputRef} />;
// 2. Хранение мутабельного значения (без ре-рендера)
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(tick, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
};Ключевое отличие от useState: изменение
ref.currentне вызывает ре-рендер. Используйrefдля значений, которые не должны обновлять UI.
Разница между управляемыми и неуправляемыми инпутами
Управляемый (controlled) — значение контролируется React через value и onChange:
const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />Неуправляемый (uncontrolled) — значение хранится в DOM, доступ через ref:
const inputRef = useRef(null);
// получить значение при сабмите:
const handleSubmit = () => console.log(inputRef.current.value);
<input ref={inputRef} defaultValue="начальное" />| Параметр | Управляемый | Неуправляемый |
|---|---|---|
| Источник правды | React state | DOM |
| Обновление UI | При каждом вводе (ре-рендер) | Нет ре-рендеров |
| Валидация | В реальном времени | При сабмите |
| Использовать когда | Формы с динамической валидацией, зависимые поля | Большие формы (40+ полей), простые формы |
Проблема производительности: 40 управляемых инпутов → ре-рендер формы при каждом нажатии клавиши. Решение: Formik или кастомный хук.
Как валидировать форму без лишних ре-рендеров?
Подход 1 — кастомный хук с изолированным состоянием:
const useValidation = (schema) => {
const validate = (values) => {
// Проверка по схеме (например, Yup)
try {
schema.validateSync(values, { abortEarly: false });
return {};
} catch (err) {
return err.inner.reduce((acc, e) => ({ ...acc, [e.path]: e.message }), {});
}
};
return { validate };
};
// Использование
const { validate } = useValidation(yupSchema);
const errors = validate(formValues); // вызываем только при сабмитеПодход 2 — изоляция состояния по полям:
// Каждый Input хранит своё состояние и валидирует себя сам
const Field = ({ name, rules }) => {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
setError(rules(e.target.value));
};
return <input value={value} onChange={handleChange} />;
};
// Ре-рендерится только этот Field, не вся формаПодход 3 — Formik:
- Оптимизирован для больших форм.
- Не ре-рендерит всю форму при вводе в одно поле.
- Интегрируется с Yup для декларативной валидации.
const form = useFormik({
initialValues: { email: '' },
validationSchema: Yup.object({ email: Yup.string().email().required() }),
onSubmit: (values) => console.log(values),
});