React Hooks: полный разбор useState, useEffect, useMemo, useCallback — что спрашивают на собеседовании

Глубокий гид по React Hooks для собеседований: useState, useEffect, useMemo, useCallback, частые ошибки, реальные вопросы интервью и практические задачи.

26 февраля 2026 г.21 минLexicon Team

React Hooks остаются самым частым блоком на frontend-интервью: для junior это база, для middle — глубина понимания механики рендера, зависимостей и производительности. На практике именно по хукам чаще всего отсеивают кандидатов: человек может писать UI, но «сыпется» на вопросах про dependency array, стабильность ссылок и лишние ререндеры.

В этой статье разберем React hooks вопросы в формате, который близок к реальному собеседованию: теория, код, типичные ошибки и объяснение «почему так работает внутри». Отдельно сделан большой раздел useEffect (самый частый интент вроде useEffect собеседование), а также блок useMemo vs useCallback с практикой оптимизации.

Если вам нужно react хуки объяснение простыми словами, но без потери технической точности — это как раз такой материал: от базы до middle-уровня аргументации.

Если хотите глубже разобрать именно механику рендеринга и триггеры перерисовок, откройте React rendering: когда компонент перерисовывается. А для фокусного разбора мемоизации с типичными ошибками посмотрите React.memo, useMemo и useCallback: оптимизация без магии.

Тренировка React Hooks перед собеседованием

Тренируйтесь на нашей платформе: реальные вопросы по хукам, фидбек по ответам и разбор ошибок уровня Junior/Middle.

Начать тренировку

1. Введение

Почему React Hooks — это примерно 70% вопросов на интервью? Потому что хуки лежат в центре современной React-разработки: управление состоянием, сайд-эффекты, оптимизация и архитектура компонент. Даже если в вакансии не пишут «нужен глубокий React performance», в живом разговоре почти всегда проверяют, понимаете ли вы:

  • как и когда компонент ререндерится;
  • почему dependency array может сломать логику;
  • когда оптимизация уместна, а когда это premature optimization;
  • как отличить корректный useEffect от антипаттерна.

Для middle-позиции недостаточно «знаю синтаксис». Нужен причинно-следственный уровень: вы должны объяснить, почему поведение происходит, какие риски есть в edge-case, и как вы принимаете архитектурные решения.

Дальше пойдет структура, которая закрывает и теорию, и практику: история появления хуков, useState, useEffect, useMemo, useCallback, связь с React.memo, типовые interview questions hooks, задачи и FAQ.

2. Что такое React Hooks

2.1 История появления (React 16.8)

До React 16.8 основным способом работы с состоянием были классовые компоненты. У этого подхода были реальные проблемы:

  • Логика одного сценария часто размазывалась по lifecycle-методам (componentDidMount, componentDidUpdate, componentWillUnmount).
  • Переиспользование stateful-логики требовало HOC или render props, что создавало вложенность и усложняло чтение.
  • Работа с this и биндингом методов создавала лишнюю когнитивную нагрузку.

Hooks решили это, добавив state и эффекты в функциональные компоненты. В результате логика стала группироваться «по смыслу», а не «по фазам жизненного цикла».

2.2 Главные принципы Hooks

  1. Хуки используются в функциональных компонентах.
  2. Хуки вызываются только на верхнем уровне.
  3. Хуки вызываются только внутри React-компонентов или кастомных хуков.

Эти правила не «про стиль», а про корректность внутренней модели React.

2.3 Почему нельзя вызывать хуки условно

React сопоставляет состояние хука не по имени переменной, а по порядку вызовов в компоненте. Если порядок меняется между рендерами, React начинает подставлять не то состояние не в тот хук.

Плохой пример:

function Profile({ isAdmin }: { isAdmin: boolean }) {
  const [name, setName] = useState("");

  if (isAdmin) {
    useEffect(() => {
      console.log("admin mode");
    }, []);
  }

  const [age, setAge] = useState(18);
  return <div>{name} {age}</div>;
}

Если isAdmin изменится, порядок хуков «съедет», и состояние станет неконсистентным. Отсюда правило: никаких условных вызовов хуков.

3. useState — глубже, чем просто «переменная»

3.1 Что спрашивают на собеседовании

Типичные вопросы:

  • Как работает batching?
  • Почему state кажется асинхронным?
  • В чем разница между setState(value) и setState(prev => ...)?
  • Когда реально происходит ререндер?

Ключевая мысль: вызов setState ставит обновление в очередь. React может объединять несколько обновлений (batching), чтобы выполнить меньше рендеров.

3.2 Как React хранит state внутри Fiber (концептуально)

На уровне концепции у каждого компонента (Fiber-узла) есть связанный список/цепочка хуков. Каждый useState хранит:

  • текущее значение;
  • очередь pending-обновлений;
  • ссылку на следующий хук.

Во время рендера React проходит хуки в порядке вызова и применяет очередь обновлений. Поэтому порядок вызова критичен.

3.3 Типичные ошибки

  1. Мутация объекта состояния:
// Плохо
user.name = "Max";
setUser(user);

Нужно создавать новую ссылку:

setUser((prev) => ({ ...prev, name: "Max" }));
  1. Зависимость от старого значения без функционального апдейта.
  2. Ожидание, что несколько setState подряд всегда отработают «по шагам», хотя React может батчить.

3.4 Реальный вопрос с интервью

Почему при двойном setCount(count + 1) счетчик увеличивается только на 1?

Проблема в том, что оба вызова используют одно и то же «старое» значение count из текущего рендера.

setCount(count + 1);
setCount(count + 1); // снова то же значение

Правильный вариант:

setCount((prev) => prev + 1);
setCount((prev) => prev + 1);

Здесь каждое обновление вычисляется из актуального предыдущего состояния в очереди.

4. useEffect — самая сложная тема на собеседовании

4.1 Lifecycle в функциональных компонентах

Через useEffect можно выразить сценарии:

  • mount: код при первом появлении компонента;
  • update: код при изменении зависимостей;
  • unmount: cleanup перед удалением компонента.

Важно: useEffect — это не «новый lifecycle API», а механизм синхронизации React-компонента с внешним миром.

4.2 Как работает dependency array

  1. useEffect(fn) без массива: эффект запускается после каждого рендера.

  2. useEffect(fn, []): эффект запускается после первого рендера (в production), cleanup — при размонтировании.

  3. useEffect(fn, [value]): эффект запускается после первого рендера и затем каждый раз, когда value изменился.

Ошибка большинства кандидатов — думать, что dependencies это «ручная оптимизация». На деле это декларация: от каких значений зависит эффект.

4.3 Почему возникает бесконечный ререндер

Пример:

useEffect(() => {
  setCount(count + 1);
}, [count]);

Механика:

  1. Рендер с count = 0.
  2. Эффект вызывает setCount(1).
  3. Компонент ререндерится.
  4. count изменился -> эффект снова запускается.
  5. Цикл бесконечный.

Как избегать:

  • Проверять условие перед setState.
  • Использовать другой источник триггера.
  • Переосмысливать, нужен ли effect вообще (часто вычисление можно сделать прямо в рендере или useMemo).

4.4 Cleanup-функция

Cleanup вызывается:

  • перед следующим выполнением эффекта (если зависимости изменились);
  • при размонтировании компонента.

Пример с подпиской:

useEffect(() => {
  const handler = () => console.log(window.innerWidth);
  window.addEventListener("resize", handler);

  return () => {
    window.removeEventListener("resize", handler);
  };
}, []);

Если cleanup не сделать, появляются утечки: старые обработчики остаются висеть и могут дублировать логику.

4.5 useEffect и Strict Mode

В dev-режиме React Strict Mode может вызывать эффекты дважды (mount -> cleanup -> mount) для проверки устойчивости побочных эффектов. Это не баг production-режима, а механизм выявления проблем:

  • отсутствие cleanup;
  • неидемпотентные побочные действия;
  • скрытые побочные эффекты в рендере.

4.6 Частые вопросы на useEffect собеседование

В чем разница между useEffect и useLayoutEffect? useEffect выполняется после paint, useLayoutEffect — синхронно после изменений DOM, но до paint. Второй нужен редко, в основном для измерений/синхронной правки layout.

Когда эффект выполняется относительно paint? useEffect — после того, как браузер отрисовал кадр.

Можно ли делать async напрямую в эффекте? Функция эффекта должна вернуть cleanup-функцию или undefined, а async всегда возвращает Promise. Поэтому напрямую писать useEffect(async () => {}) нельзя.

Корректный вариант:

useEffect(() => {
  let canceled = false;

  async function load() {
    try {
      const res = await fetch("/api/data");
      const data = await res.json();
      if (!canceled) {
        // setState(data)
      }
    } catch (e) {
      if (!canceled) {
        // setError(e)
      }
    }
  }

  load();
  return () => {
    canceled = true;
  };
}, []);

4.7 Stale closure: почему effect видит «старое» значение

Один из самых частых вопросов уровня middle: почему внутри колбэка или таймера значение состояния «устарело».

Пример:

useEffect(() => {
  const id = setInterval(() => {
    console.log(count); // может логировать старое значение
  }, 1000);

  return () => clearInterval(id);
}, []);

Здесь effect с пустым массивом зависимостей захватывает count из первого рендера. Решения зависят от задачи:

  • добавить count в dependencies и пересоздавать интервал;
  • использовать ref для хранения актуального значения;
  • пересмотреть архитектуру, чтобы не полагаться на «долгоживущий» колбэк со старыми данными.

На интервью важно проговорить саму причину: не «React багует», а обычная механика замыканий JavaScript.

4.8 Race conditions в запросах и защита от них

Еще один практический кейс: пользователь быстро меняет фильтр, а ответы приходят в другом порядке. В итоге интерфейс может показать устаревшие данные.

Пример проблемы:

  1. Запрос A уходит первым.
  2. Запрос B уходит позже.
  3. B возвращается быстрее, UI обновляется.
  4. Потом приходит A и перетирает UI старой версией.

Типовые решения:

  • AbortController для отмены старого запроса;
  • проверка флага актуальности в cleanup;
  • библиотека data-fetching с кэшом и дедупликацией.

Пример с AbortController:

useEffect(() => {
  const controller = new AbortController();

  async function load() {
    const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
    const data = await res.json();
    setItems(data);
  }

  load().catch((e) => {
    if (e.name !== "AbortError") {
      setError("Ошибка загрузки");
    }
  });

  return () => controller.abort();
}, [query]);

Такой ответ на собеседовании сразу показывает практическую зрелость и понимание react performance hooks в контексте сетевого слоя.

5. useMemo — оптимизация вычислений

5.1 Что делает useMemo

useMemo мемоизирует значение: React кэширует результат функции и пересчитывает его только при изменении зависимостей.

const filtered = useMemo(() => {
  return products.filter((p) => p.price > minPrice);
}, [products, minPrice]);

5.2 Когда действительно нужен

  • Есть тяжелое вычисление (фильтрация/сортировка больших списков).
  • Нужно стабилизировать ссылку на значение, чтобы не триггерить дочерние компоненты без необходимости.

5.3 Когда вреден

  • Микрооптимизация там, где расчеты дешевы.
  • Усложнение кода ради «ощущения performance».
  • Ошибочные зависимости, которые дают устаревшее значение.

5.4 Вопросы по useMemo

Можно ли убрать useMemo? Да, если нет проблемы производительности и без него код проще.

Что если dependency не указать? Тогда мемоизация теряет смысл или ломается корректность данных.

useMemo — гарантия или подсказка React? Практически это оптимизация, а не бизнес-гарантия. Нельзя строить критичную логику на предположении «useMemo обязательно сработает как кэш навсегда».

6. useCallback — стабилизация функций

6.1 Что делает useCallback

useCallback мемоизирует функцию и возвращает стабильную ссылку, пока зависимости не изменились.

const onSelect = useCallback((id: string) => {
  setSelectedId(id);
}, []);

6.2 useMemo vs useCallback

useMemouseCallback
Возвращает значениеВозвращает функцию
Мемоизирует результат вычисленияМемоизирует ссылку на функцию

Ключевая формулировка для интервью: useCallback(fn, deps) эквивалентен useMemo(() => fn, deps).

6.3 Почему функции ломают React.memo

Если родитель на каждом рендере создает новую функцию, дочерний компонент в React.memo видит новый prop по ссылке и ререндерится.

const Child = React.memo(function Child({ onClick }: { onClick: () => void }) {
  console.log("Child render");
  return <button onClick={onClick}>Click</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Child onClick={handleClick} />
    </>
  );
}

6.4 Когда useCallback бесполезен

  • Если дочерний компонент не мемоизирован.
  • Если функция не уходит вниз по дереву как prop.
  • Если ререндеры и так дешевы и не являются bottleneck.

7. React.memo + Hooks — любимая комбинация интервьюеров

Частый сценарий interview questions hooks: «Почему оптимизация не сработала, хотя добавили React.memo

Причины:

  • В props передаются новые объекты/массивы/функции на каждый рендер.
  • Ошибочные dependencies в useMemo/useCallback.
  • Компонент ререндерится из-за изменения context.

React.memo делает shallow comparison props. Это означает: сравнение по ссылкам для объектов и функций. Если ссылка новая, мемо не спасет.

Паттерн работы:

  1. React.memo для тяжелого дочернего компонента.
  2. useMemo для стабильных вычисляемых данных, которые передаются вниз.
  3. useCallback для стабильных обработчиков.
  4. Проверка в profiler, что эффект реально есть.

Мини-чеклист перед оптимизацией:

  1. Есть ли измеримая проблема (время рендера, лаг интерфейса)?
  2. Понимаете ли вы, какой компонент действительно дорогой?
  3. Изменит ли React.memo поведение с учетом текущих props?
  4. Не станет ли код сложнее, чем выигрыш в миллисекундах?

Это важно для интентов оптимизация react и react performance hooks: хороший кандидат сначала измеряет, потом оптимизирует.

8. Самые частые ошибки на собеседовании

  • Путают useMemo и useCallback.
  • Не понимают dependency array и «добавляют только чтобы ESLint замолчал».
  • Думают, что useEffect(() => {}, []) это просто полный аналог componentDidMount.
  • Мутируют state и удивляются некорректным рендерам.
  • Вызывают хуки условно.
  • Используют useEffect для вычислений, которые можно делать без side-effect.

Отдельно для SEO-интента лишние ререндеры react: главная причина лишних ререндеров — нестабильные ссылки и отсутствие архитектурной границы между данными и представлением.

9. Как отвечать на интервью по хукам

Формула сильного ответа:

  1. Короткое определение.
  2. Как работает внутри.
  3. Когда использовать.
  4. Типичная ошибка.
  5. Мини-пример.

Пример идеального ответа на вопрос:

В чем разница между useMemo и useCallback?

«useMemo мемоизирует результат вычисления, а useCallback мемоизирует функцию, то есть ссылку на колбэк. Я использую useMemo, когда есть тяжелые вычисления или когда важно стабилизировать объект/массив в props. useCallback применяю вместе с React.memo, когда передаю обработчик в дочерний компонент и хочу избежать лишних ререндеров. Частая ошибка — использовать их везде без профилирования, что усложняет код без пользы.»

10. Практический блок

Задача 1. Найти баг в useEffect

useEffect(() => {
  fetchUser(userId).then(setUser);
}, []);

Что не так: userId отсутствует в dependencies, при смене userId данные не обновятся.

Задача 2. Оптимизировать список с useMemo

Дано: при каждом вводе в поле поиска происходит дорогая сортировка большого списка. Решение: вынести сортировку/фильтрацию в useMemo с корректными зависимостями.

Пример:

const preparedUsers = useMemo(() => {
  return users
    .filter((u) => u.name.toLowerCase().includes(query.toLowerCase()))
    .sort((a, b) => a.score - b.score);
}, [users, query]);

Задача 3. Починить лишние ререндеры

Дано: React.memo не помогает. Проверить, не создаются ли новые функции и объекты в родителе на каждом рендере. Стабилизировать useCallback/useMemo там, где это обосновано.

Разбор на интервью обычно ждут такой:

  • подтвердить лишние ререндеры в React DevTools Profiler;
  • показать конкретный prop, который всегда новый по ссылке;
  • исправить точечно, а не «обернуть все подряд».

Задача 4. Объяснить бесконечный цикл

useEffect(() => {
  setFilters({ ...filters, page: 1 });
}, [filters]);

Каждый setFilters создает новый объект, filters меняется, эффект снова срабатывает. Нужно изменить логику триггера и condition, либо убрать effect.

Задача 5. Переписать код без лишних хуков

Если вычисление дешевое и не передается в мемоизированных детей, удалить useMemo/useCallback. Цель: код должен быть простым, а не «формально оптимизированным».

Типичный антипример:

const title = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

Если это простая строка без тяжелых вычислений и без влияния на дорогие дочерние компоненты, useMemo не нужен. На собеседовании такой рефакторинг часто оценивают как признак инженерной зрелости.

Мок-интервью по React Hooks

Получите разбор ответов по useEffect, useMemo, useCallback и поймите, где именно теряете баллы на интервью.

Пройти интервью-тренировку

12. Заключение

React hooks — фундамент современной разработки и ключевой фильтр на интервью. Без понимания dependency array, ссылочной стабильности и механики рендера сложно пройти middle-уровень. Если хотите уверенно отвечать на react interview questions hooks, отрабатывайте не только определения, но и практические кейсы: бесконечные циклы, оптимизация react, react performance hooks, диагностика лишних ререндеров.

Лучший формат подготовки — короткий цикл: изучили тему -> решили задачу -> получили фидбек -> повторили. Чтобы ускорить прогресс, тренируйтесь на нашей платформе и фиксируйте ответы в формате реального собеседования.

Больше разборов в Telegram

Ежедневные вопросы по React, мини-задачи и разборы типичных ошибок на собеседованиях.

Перейти в Telegram

Автор

Lexicon Team

Читайте также