React rendering: когда компонент перерисовывается

Подробный разбор рендеринга в React: когда компонент перерисовывается, когда нет, как работают state/props/context, React.memo, useMemo и что отвечать на собеседовании.

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

Введение

Тема рендеринга в React стабильно входит в топ вопросов на технических интервью. Причина простая: по ответу на этот блок сразу видно, понимает ли кандидат, как работает React под капотом, или просто знает API на уровне синтаксиса.

Большая часть джунов путает базовые вещи: считают, что любой рендер равен обновлению DOM, не различают триггеры ререндера и не могут объяснить, почему дочерний компонент рендерится "без видимой причины". Из-за этого даже хорошие по коду кандидаты теряют баллы на секции architecture/performance.

Ключевая разница, которую важно понимать с самого начала: "перерисовка" компонента в React - это повторный проход рендер-фазы, а не обязательно физическое изменение DOM-узлов в браузере. React может пересчитать дерево, сравнить его с прошлым и не сделать ни одного DOM-апдейта.

В этой статье разберем, что такое рендер в React, когда компонент перерисовывается и когда нет, где помогают React.memo, useMemo, useCallback, как не попасть в типичные ловушки и как уверенно отвечать на собеседовании.

Если вам нужна база по общим вопросам уровня junior, держите рядом большой разбор Junior React вопросов. А для уверенного блока по хукам используйте подробный гайд по React Hooks для интервью. Для темы Virtual DOM с нуля используйте Virtual DOM в React простыми словами. Если вы готовитесь к middle-собеседованию именно по performance, переходите к Оптимизация React: что спрашивают на middle-собеседованиях. А для системного понимания фаз mount/update/unmount откройте Жизненный цикл компонента в React в 2026 году.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

1. Что такое рендер в React на самом деле

1.1 Render != обновление DOM

Когда React "рендерит" компонент, это не означает мгновенное изменение DOM. Процесс состоит из нескольких этапов:

  • Вычисление нового дерева элементов (Virtual DOM).
  • Сравнение нового дерева со старым (Reconciliation).
  • Применение реальных изменений в DOM (Commit phase), если изменения вообще есть.

Именно поэтому один и тот же компонент может ререндериться много раз, а DOM при этом изменится только там, где действительно есть разница.

Короткая формула для интервью:

  1. Render phase: React вычисляет, что должно быть на экране.
  2. Reconciliation: React определяет, что изменилось.
  3. Commit phase: React применяет изменения в DOM.

1.2 Что происходит при рендере функционального компонента

При ререндере функционального компонента React повторно вызывает функцию компонента. Внутри происходит:

  • Повторный вызов всей функции.
  • Повторный расчет JSX.
  • Пересоздание объектов/массивов/функций, объявленных прямо в теле компонента.

Простой пример:

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

  console.log("Counter render");

  const style = { color: "tomato" }; // новая ссылка на каждый рендер
  const onClick = () => setCount((c) => c + 1); // новая функция на каждый рендер

  return (
    <button style={style} onClick={onClick}>
      {count}
    </button>
  );
}

Вывод в консоль покажет количество рендеров компонента, но это не равно количеству изменений DOM.

2. Когда компонент перерисовывается

2.1 Изменение state (useState)

Самый частый триггер - обновление состояния через setState/setCount.

Важные нюансы:

  • setState планирует обновление, а не "мгновенно меняет DOM".
  • В React 18 действует automatic batching: несколько обновлений в одном тике часто объединяются.
  • Обновление с тем же значением может быть пропущено.

Пример batching:

function Example() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const handleClick = () => {
    setA((x) => x + 1);
    setB((x) => x + 1);
    // Обычно это приведет к одному рендеру, а не к двум
  };

  return <button onClick={handleClick}>{a}:{b}</button>;
}

setState всегда вызывает рендер? Практический ответ: React ставит обновление в очередь, но если новое значение эквивалентно старому, коммит может не произойти.

2.2 Изменение props

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

Критичный момент: для объектов и функций важно сравнение по ссылке, а не по содержимому.

function Parent() {
  const data = { role: "admin" }; // новая ссылка на каждый рендер
  return <Child data={data} />;
}

Даже если внутри "то же значение", для React это новый объект.

2.3 Ререндер родителя

По умолчанию ререндер родителя означает повторный рендер детей. Это нормальное поведение React.

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Child />
    </>
  );
}

function Child() {
  console.log("Child render");
  return <div>Child</div>;
}

Child будет логировать рендер при каждом обновлении Parent, пока вы не добавите механизмы оптимизации.

2.4 Context

Изменение значения context ререндерит подписчиков этого контекста.

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

Практика:

  • Делите контексты по зонам ответственности.
  • Мемоизируйте value провайдера, когда это уместно.
  • Не складывайте "весь app state" в один context.

2.5 Force update (редкие кейсы)

Принудительные обновления используются редко и обычно сигнализируют о проблеме архитектуры. В функциональном стиле это чаще обходные паттерны вроде setTick((x) => x + 1) ради перерендера.

На интервью такой подход стоит обозначать как крайний случай, а не норму.

3. Когда компонент НЕ перерисовывается

3.1 React.memo

React.memo мемоизирует результат рендера компонента по shallow compare пропсов.

const UserCard = React.memo(function UserCard({ name }: { name: string }) {
  console.log("UserCard render");
  return <div>{name}</div>;
});

Если name не изменился, React может пропустить рендер.

Где ломаются ожидания:

  • Передали объект/функцию с новой ссылкой.
  • Компонент читает context, который обновился.
  • Неправильный custom comparator.

3.2 useMemo

useMemo мемоизирует вычисление, а не "замораживает" компонент.

const filtered = useMemo(() => heavyFilter(items, query), [items, query]);

Компонент может ререндериться, но дорогое вычисление не будет пересчитано без изменения зависимостей.

3.3 useCallback

useCallback мемоизирует ссылку на функцию.

Это особенно полезно, когда функция передается в дочерний React.memo-компонент.

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

Без useCallback в ряде кейсов React.memo у ребенка не даст эффекта, потому что каждый раз приходит новый callback.

3.4 useRef

Изменение ref.current не запускает рендер.

const countRef = useRef(0);
countRef.current += 1; // UI сам по себе не обновится

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

4. Частые ошибки, которые валят на интервью

  • "Компонент рендерится только если изменился state" - неверно: есть props, context, ререндер родителя и другие триггеры.
  • Путаница useMemo и React.memo.
  • Мутация state вместо создания новой ссылки.
  • Инлайн-функции в пропсах без понимания последствий для referential equality.
  • Непонимание сравнения по ссылке для объектов/массивов/функций.

Если хотите системно закрыть блок по хукам и этим ошибкам, дополнительно пройдите раздел про useEffect/useMemo/useCallback.

5. Разбор на реальном примере

Возьмем типовой кейс с собеседования.

Шаг 1. Родитель с кнопкой и дочерний компонент

function Parent() {
  const [count, setCount] = useState(0);
  const [theme, setTheme] = useState("light");

  console.log("Parent render");

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
        Theme: {theme}
      </button>
      <Child label="Profile" onOpen={() => console.log("open")} />
    </div>
  );
}

function Child({ label, onOpen }: { label: string; onOpen: () => void }) {
  console.log("Child render");
  return <button onClick={onOpen}>{label}</button>;
}

Здесь Child ререндерится при любом рендере Parent.

Шаг 2. Оптимизация через React.memo

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

Частая ошибка: кажется, что теперь Child перестанет ререндериться. Но нет, потому что onOpen={() => ...} в родителе создает новую функцию на каждом рендере.

Шаг 3. Добавление useCallback

function Parent() {
  const [count, setCount] = useState(0);
  const [theme, setTheme] = useState("light");

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

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
        Theme: {theme}
      </button>
      <Child label="Profile" onOpen={handleOpen} />
    </div>
  );
}

Теперь при изменении count/theme Child сможет пропускать лишние ререндеры, если его пропсы по ссылкам стабильны.

Шаг 4. Анализ логов

Что ожидаем увидеть:

  • До оптимизации: Parent render + Child render почти на каждое действие.
  • После React.memo + useCallback: Parent render часто, Child render только при реальных изменениях его пропсов.

Это хороший демонстрационный блок на интервью: вы показываете не только теорию, но и диагностику через логи/Profiler.

6. React 18: что изменилось в рендеринге

Automatic batching

React 18 объединяет больше обновлений в один рендер-цикл, включая асинхронные сценарии (в ряде случаев), что снижает количество лишних перерисовок.

Concurrent rendering (базовое понимание)

Concurrent rendering позволяет React планировать работу гибче: приоритизировать более важные обновления и делать интерфейс отзывчивее под нагрузкой.

Для интервью достаточно базовой мысли: это не "все стало параллельным", а модель планирования рендера с приоритетами и возможностью прерывать менее важные задачи.

StrictMode двойной вызов в dev

В dev-режиме под StrictMode React может дважды вызывать рендер/эффекты для выявления побочных эффектов и неидемпотентной логики.

Это частая ловушка на интервью: кандидат думает, что в проде компонент "ломается", хотя это проверочное поведение dev-среды.

7. Как правильно отвечать на собеседовании

Структура сильного ответа:

  1. Что такое рендер.
  2. Основные триггеры (state, props, parent, context).
  3. Как оптимизировать (React.memo, useCallback, useMemo по месту).
  4. Подводные камни (ссылочное равенство, мутации, StrictMode).

Пример ответа уровня middle:

"В React рендер компонента - это повторное выполнение функции компонента и пересчет дерева, а не обязательно изменение DOM. Основные триггеры: обновление state, новые props, ререндер родителя и изменение context. По умолчанию дети ререндерятся вместе с родителем. Для оптимизации я сначала измеряю узкие места в Profiler, затем точечно применяю React.memo и стабилизирую пропсы через useCallback/useMemo. Также проверяю, нет ли мутаций state и лишних пересозданий объектов."

8. Практические советы для продакшена

Когда НЕ нужно оптимизировать

Не начинайте с мемоизации "всего подряд". Если компонент легкий и нет проблем в метриках, лишняя оптимизация только усложняет код.

Как искать лишние ререндеры

  • Временные console.log на проблемных компонентах.
  • React DevTools Profiler.
  • Проверка стабильности пропсов (особенно функций/объектов).

React DevTools Profiler

Profiler показывает, какие компоненты и почему ререндерятся, сколько времени это занимает и какие оптимизации реально дали эффект.

Почему premature optimization - зло

Преждевременная оптимизация без измерений приводит к сложному коду и ложному ощущению контроля производительности. Сначала факт проблемы, потом точечное решение.

Тренировка React-базы перед собеседованием

Отработайте блоки по рендерингу, хукам и архитектуре в формате мок-интервью с разбором ответов.

Прокачать React-базу

9. FAQ

Вызывает ли setState с тем же значением ререндер?

Обычно React может пропустить обновление, если значение не изменилось по Object.is. Но в dev-режиме вы можете видеть дополнительные вызовы из-за проверок StrictMode.

Перерисуется ли компонент, если изменить объект внутри state?

Если объект мутирован по месту и ссылка не изменилась, React может не увидеть обновление. Правильный путь - иммутабельное обновление с новой ссылкой.

Ререндерится ли memo-компонент при изменении контекста?

Да. React.memo работает по пропсам, но изменение контекста ререндерит подписчика useContext.

useMemo гарантирует, что компонент не ререндерится?

Нет. useMemo кэширует вычисление, но не отменяет рендер компонента.

10. Итоги

5 главных правил:

  1. Рендер компонента и обновление DOM - не одно и то же.
  2. Триггеры рендера: state, props, parent render, context.
  3. React.memo работает только при стабильных пропсах по ссылке.
  4. useMemo оптимизирует вычисления, useCallback - ссылки на функции.
  5. Оптимизации применяются после измерений, а не "на всякий случай".

Чеклист перед собеседованием:

  • Можете объяснить render/reconciliation/commit простыми словами.
  • Можете назвать все основные триггеры ререндера.
  • Можете разобрать кейс с React.memo + useCallback.
  • Понимаете ограничения useMemo и useRef.
  • Понимаете, что меняется в React 18 (batching, StrictMode, concurrent model).

Что нужно уметь объяснить без запинки:

  • Почему ререндер не равен DOM-апдейту.
  • Почему ребенок ререндерится при ререндере родителя.
  • Почему новая ссылка на объект/функцию ломает мемоизацию.
  • Почему мутация state ведет к багам.

Подборки React-вопросов в Telegram

Регулярные подборки вопросов для собеседований и короткие разборы сильных ответов.

Перейти в Telegram

Автор

Lexicon Team

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