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 — хранит изменяемое значение без ре-рендера; даёт доступ к DOM
  • useCallback — мемоизирует функции, чтобы избежать лишних ре-рендеров
  • useMemo — мемоизирует вычисляемые значения
  • useContext — читает значение из Context без дополнительных пропсов
  • useLayoutEffect — как useEffect, но срабатывает синхронно после обновления DOM (до отрисовки браузером)
  • forwardRef — не хук, а обёртка (HOC): передаёт ref через компонент вниз. (В React 19 ref можно передавать как обычный проп, forwardRef больше не нужен.)

Как useEffect заменяет методы жизненного цикла классов?

КлассХуки
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdateuseEffect(() => { ... }, [deps]) — срабатывает и после первого рендера, если deps заданы; для «только обновления» вручную сравнивают с ref предыдущих значений
componentWillUnmountreturn () => { ... } из useEffect
componentDidMount + componentWillUnmountодин useEffect с пустым массивом и cleanup

Нюансы по сравнению с классами:

  • Один useEffect на одну «задачу» (подписка, запрос, таймер) — проще сочетать несколько эффектов, чем пихать всё в componentDidMount.
  • Эффект после отрисовки (асинхронно относительно браузера), не блокирует paint.
  • При смене зависимостей: сначала cleanup старого эффекта, затем новый эффект.

В чём разница между useEffect и useLayoutEffect?

useEffectuseLayoutEffect
Когда выполняетсяПосле того, как браузер уже отрисовал изменения (асинхронно, не блокирует 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 изменился

Порядок работы:

  1. React рендерит компонент → обновляет DOM
  2. Браузер отображает изменения
  3. React запускает useEffect (асинхронно, после paint)
  4. При следующем рендере сначала выполняется 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/propsuseMemo
Нужен 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 stateDOM
Обновление 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),
});