Перейти к содержанию
Фронтенд

42 вопроса по теме «React» на собеседовании

В этом материале — 42 вопроса из русской колоды RecallDeck по теме «React». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

35 мин чтения42 подробных ответаПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

42 подробных ответа

01

Что такое React и какую проблему он решает?

Короткий ответ: React — это JavaScript-библиотека для построения пользовательских интерфейсов через композицию переиспользуемых компонентов. Её главная идея — UI как функция от состояния: UI = f(state).

Подробно:

React — это библиотека (не фреймворк), разработанная Facebook (Meta). Она отвечает за слой представления (View) и не диктует, как организовывать роутинг, запросы к серверу или управление состоянием — это даёт гибкость, но требует подключения дополнительных библиотек.

Ключевые принципы:

  • Декларативность — вы описываете что должно отображаться при данном состоянии, а не как пошагово менять DOM.
  • Компонентность — UI собирается из независимых переиспользуемых блоков.
  • Однонаправленный поток данных — данные текут сверху вниз (от родителя к детям).
// Описываем КАК выглядит UI при данном state, а не как его менять
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Кликов: {count}</button>;
}

⚠️ Ловушка: React часто называют «фреймворком». На собеседовании корректнее сказать «библиотека для UI». Angular — это фреймворк (роутинг, DI, формы, HTTP «из коробки»), а React закрывает только View.

02

Что значит «React декларативный»?

Короткий ответ: Вы описываете желаемый конечный результат (UI для данного состояния), а React сам вычисляет, какие изменения внести в DOM. Императивный подход — это ручное пошаговое манипулирование DOM.

Подробно:

// Императивно (vanilla JS): говорим КАК менять
const btn = document.getElementById('btn');
let count = 0;
btn.addEventListener('click', () => {
  count++;
  btn.textContent = `Кликов: ${count}`; // вручную обновляем DOM
});
// Декларативно (React): говорим ЧТО показать
function Btn() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Кликов: {count}</button>;
}

Преимущества декларативности: меньше багов из-за рассинхронизации DOM и данных, код предсказуем и читается как «снимок» UI.

⚠️ Ловушка: Декларативность не бесплатна — за неё платим работой Virtual DOM и diffing'ом. Но в большинстве приложений это выгодный размен на поддерживаемость.

03

Что такое компонентный подход и SPA?

Короткий ответ: Компонент — это самодостаточный кусок UI со своей логикой и разметкой. SPA (Single Page Application) — приложение, которое загружает одну HTML-страницу и динамически перерисовывает контент без перезагрузки страницы.

Подробно:

// Композиция: маленькие компоненты собираются в большие
function Avatar({ url }) { return <img className="avatar" src={url} />; }
function UserCard({ user }) {
  return (
    <div className="card">
      <Avatar url={user.avatar} />
      <span>{user.name}</span>
    </div>
  );
}
function App() {
  return <UserCard user={{ name: 'Аня', avatar: '/a.png' }} />;
}

В SPA при навигации обновляется только нужная часть DOM (через client-side routing, например React Router), а не загружается новая страница с сервера. Это даёт ощущение «нативного» приложения.

⚠️ Ловушка: У SPA есть минусы: хуже базовое SEO (контент рендерится JS), большой initial bundle, нужна гидратация для SSR. Часть проблем решается через SSR/SSG (Next.js).

04

Что такое JSX и как он транспилируется?

Короткий ответ: JSX — это синтаксический сахар над вызовами создания элементов. Браузер JSX не понимает; Babel/SWC транспилируют его в React.createElement(...) (классический runtime) или в jsx(...) из react/jsx-runtime (новый automatic runtime с React 17+).

Подробно:

// Пишем JSX
const el = <h1 className="title">Привет, {name}!</h1>;

// Классическая транспиляция (до React 17):
const el = React.createElement('h1', { className: 'title' }, 'Привет, ', name, '!');

// Новый automatic runtime (React 17+), импорт добавляется автоматически:
import { jsx as _jsx } from 'react/jsx-runtime';
const el = _jsx('h1', { className: 'title', children: ['Привет, ', name, '!'] });

React.createElement возвращает обычный JS-объект (React element) — лёгкое описание того, что должно быть на экране: { type, props, key, ... }. Это НЕ DOM-узел.

Правила JSX:

  • Должен быть один корневой элемент (или <>...</> — Fragment).
  • classclassName, forhtmlFor (т.к. class/for — зарезервированные слова).
  • Атрибуты в camelCase: onClick, tabIndex.
  • Выражения в {}, но только expressions (не statements: нельзя if, можно тернарник).
  • Компоненты — с заглавной буквы (<MyComp />), иначе React сочтёт это HTML-тегом.
// JSX — это выражение, его можно присвоить, вернуть, передать в массив
const items = list.map(x => <li key={x.id}>{x.name}</li>);

⚠️ Ловушка: С новым automatic runtime импорт React в файле больше не обязателен для JSX (раньше был нужен, т.к. вызывался React.createElement). Но если используете React.something напрямую — импорт всё ещё нужен.

05

Что такое Virtual DOM и зачем он нужен?

Короткий ответ: Virtual DOM (VDOM) — это лёгкое представление UI в виде JS-объектов в памяти. React держит VDOM, при изменении state строит новый VDOM, сравнивает его со старым (diffing) и применяет к реальному DOM только минимальный набор изменений.

Подробно:

Реальные операции с DOM дорогие (перерасчёт стилей, reflow, repaint). Прямое массовое обновление DOM тормозит. VDOM — это слой абстракции:

1. state меняется
2. React вызывает компонент → получает новое дерево VDOM (JS-объекты)
3. Сравнивает новый VDOM со старым (reconciliation)
4. Вычисляет минимальный diff
5. Применяет ТОЛЬКО изменения к реальному DOM (batched)
// При смене count React НЕ перерисовывает весь <div>,
// а меняет только текстовый узел внутри <span>
function App({ count }) {
  return (
    <div>
      <header>Заголовок</header>     {/* не тронут */}
      <span>{count}</span>           {/* обновится только это */}
    </div>
  );
}

⚠️ Ловушка: Частое заблуждение — «VDOM всегда быстрее прямого DOM». Это не так: VDOM добавляет накладные расходы (построение и сравнение деревьев). Его ценность — в предсказуемости и в том, что он избавляет разработчика от ручной оптимизации обновлений, а не в абсолютной скорости. Рукописный точечный textContent = x быстрее, но не масштабируется.

06

Как работает reconciliation (алгоритм согласования)?

Короткий ответ: Reconciliation — процесс сравнения нового дерева VDOM со старым для определения минимальных изменений. React использует эвристический O(n) алгоритм, опираясь на два допущения: элементы разных типов дают разные деревья, и стабильность узлов в списках задаётся через key.

Подробно:

Точное сравнение двух деревьев — задача O(n³). React сводит её к O(n) за счёт эвристик:

  1. Разный тип элемента → полная замена поддерева. Если <div> стал <span>, React уничтожит старый узел со всем содержимым (включая state дочерних компонентов) и создаст новый.
// При смене isLoggedIn React НЕ переиспользует старый компонент:
{isLoggedIn ? <AdminPanel /> : <LoginForm />}
// AdminPanel и LoginForm — разные типы, поддерево пересоздаётся
  1. Один тип элемента → обновление пропсов. React оставляет узел, меняет только изменившиеся атрибуты, рекурсивно сравнивает детей.

  2. Списки сравниваются по key. Без key React сопоставляет детей по позиции, что приводит к багам при вставке/удалении в начало.

⚠️ Ловушка: Условный рендеринг компонента одного типа в разных ветках с РАЗНОЙ структурой может неожиданно сохранить state. Например, два <input> в обеих ветках тернарника на одной позиции переиспользуют DOM-узел и сохраняют введённый текст. Решается разными key.

07

Зачем нужен `key` и почему не стоит использовать `index` массива?

Короткий ответ: key помогает React идентифицировать элементы списка между рендерами, чтобы корректно переиспользовать, переставлять и удалять узлы. index как key ломается при вставке/удалении/сортировке, вызывая баги со state и неэффективные обновления.

Подробно:

// ХОРОШО: стабильный уникальный id
{users.map(u => <UserRow key={u.id} user={u} />)}

// ПЛОХО: index как key
{users.map((u, i) => <UserRow key={i} user={u} />)}

Что ломается с index: если вставить элемент в начало списка, у всех элементов сдвигаются индексы. React думает, что элемент с key=0 «остался тем же», и переиспользует его DOM/state, хотя данные другие.

// Демонстрация бага: каждый input имеет внутренний state (uncontrolled)
function Todos({ items }) {
  return items.map((item, i) => (
    <li key={i}>
      {item.text}
      <input defaultValue="" /> {/* при вставке в начало текст «переедет» не туда */}
    </li>
  ));
}

Когда index допустим: список статичный, не переупорядочивается, элементы не добавляются/удаляются в середину, и нет локального state/неконтролируемого ввода.

⚠️ Ловушка: key должен быть уникален среди siblings (соседей), а не глобально. И key — это не пропс: компонент не получит props.key. Если нужно значение внутри — передавайте отдельным пропсом.

08

Что такое Fiber архитектура (кратко)?

Короткий ответ: Fiber — переписанный с React 16 движок reconciliation. Он представляет каждый элемент как «fiber» (единицу работы) и делает рендеринг прерываемым: React может разбить работу на части, приостановить её, отдать приоритет важным обновлениям и продолжить позже. Это основа Concurrent-возможностей.

Подробно:

До Fiber (стек-реконсилятор) рендеринг был синхронным и рекурсивным — начав, нельзя было прерваться, что блокировало главный поток на больших деревьях.

Fiber разбивает работу на две фазы:

  • Render/reconciliation фаза — прерываемая, без сайд-эффектов: строится дерево fiber'ов, вычисляется diff. Может быть приостановлена, прервана, перезапущена.
  • Commit фаза — синхронная, непрерываемая: изменения применяются к реальному DOM, вызываются эффекты.

Это позволяет планировать работу по приоритетам (анимация важнее загрузки данных) — на этом строятся useTransition, Suspense, time slicing.

⚠️ Ловушка: В render-фазе компонент может вызываться несколько раз или его результат может быть отброшен. Поэтому render должен быть чистым (без сайд-эффектов) — все эффекты идут в useEffect/useLayoutEffect, выполняемые в commit-фазе.

09

Функциональные vs классовые компоненты — в чём разница и почему перешли на функциональные?

Короткий ответ: Классовые — ES6-классы с методами жизненного цикла и this.state. Функциональные — обычные функции, получающие хуки для state и эффектов (с React 16.8). Сообщество перешло на функциональные: меньше шаблонного кода, нет путаницы с this, лучше переиспользование логики через хуки.

Подробно:

// Классовый компонент
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// Функциональный — то же самое
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Проблемы классов, которые решили хуки:

  • this запутан (нужен bind или стрелочные методы).
  • Логику жизненного цикла трудно переиспользовать (HOC и render props приводили к «wrapper hell»).
  • Связанная логика разбросана по разным методам (componentDidMount/componentWillUnmount), а несвязанная — смешана в одном методе.

⚠️ Ловушка: Классы не считаются устаревшими (deprecated) — старый код на них работает. Но новый код пишут на функциональных компонентах. Error boundaries до сих пор реализуются только классами (нет хук-эквивалента componentDidCatch).

10

В чём разница между props и state?

Короткий ответ: Props — входные данные, передаваемые компоненту извне (от родителя), они read-only. State — внутреннее изменяемое состояние компонента, которым он управляет сам. Изменение и того, и другого вызывает ре-рендер.

Подробно:

Props State
Откуда от родителя внутри компонента
Изменяемость неизменяемы (read-only) изменяемы (через setState/setX)
Кто меняет родитель сам компонент
function Greeting({ name }) {          // name — prop, извне
  const [count, setCount] = useState(0); // count — state, внутри
  return <p>{name}: {count}</p>;
}

⚠️ Ловушка: Нельзя мутировать props (props.name = 'x') — это нарушает однонаправленный поток и не вызовет ре-рендер у родителя. Если ребёнку нужно изменить данные родителя — родитель передаёт колбэк (lifting state up).

11

Что такое односторонний поток данных, lifting state up и props drilling?

Короткий ответ: Данные в React текут только сверху вниз (от родителя к детям через props). Lifting state up — подъём общего state в ближайшего общего предка, когда несколько компонентов должны разделять данные. Props drilling — проблема прокидывания пропсов через много промежуточных слоёв, которым они не нужны.

Подробно:

// Lifting state up: state живёт в родителе, дети получают значение и колбэк
function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <Input value={value} onChange={setValue} />
      <Preview value={value} />
    </>
  );
}

Props drilling — антипаттерн, когда пропс «проваливается» через несколько уровней:

// theme нужна только Button, но тащим через App → Page → Toolbar → Button
<Page theme={theme}><Toolbar theme={theme}><Button theme={theme} /></Toolbar></Page>

Решения props drilling: Context API, композиция (передача children), state manager.

⚠️ Ловушка: Не стоит сразу хвататься за Context при первой же передаче через 2 уровня. Сначала рассмотрите композицию (children), которая часто устраняет drilling без глобального состояния.

12

Зачем появились хуки и каковы правила их использования?

Короткий ответ: Хуки (React 16.8) позволяют использовать state и другие возможности React в функциональных компонентах и переиспользовать логику без классов и HOC. Два правила: вызывать хуки только на верхнем уровне (не в условиях/циклах/вложенных функциях) и только из React-компонентов или других хуков.

Подробно:

Хуки убрали необходимость связывать this в классах, позволили переиспользовать stateful-логику через custom hooks вместо render-props/HOC-«матрёшек» и дали возможность держать рядом код одного эффекта, а не разносить его между lifecycle-методами.

Правила хуков:

  1. Только на верхнем уровне. Нельзя вызывать хуки внутри условий, циклов, вложенных функций.
// ❌ НЕЛЬЗЯ
if (cond) { const [x, setX] = useState(0); }

// ✅ Правильно
const [x, setX] = useState(0);
if (cond) { /* используем x */ }
  1. Только из React-функций (компонентов или кастомных хуков), не из обычных функций.

Почему правило №1? React не хранит state по имени переменной — он опирается на порядок вызова хуков. Внутри React это похоже на массив/связанный список: первый useState — слот 0, второй — слот 1, и т.д. Если поставить хук под условие, порядок между рендерами «поедет», и React сопоставит state не тому хуку.

// Внутренняя модель (упрощённо):
// рендер 1: [count, name, effect]  слоты 0,1,2
// рендер 2 (если useState под if пропущен): [name, effect] — сдвиг! баг

⚠️ Ловушка: ESLint-плагин eslint-plugin-react-hooks ловит нарушения правил и пропущенные зависимости — обязателен в проекте. На собеседовании важно объяснить почему правило существует (порядок вызова), а не просто его процитировать.

13

Как работает useState? Что такое батчинг, асинхронность setState и функциональное обновление?

Короткий ответ: useState возвращает текущее значение и сеттер. Вызов сеттера не меняет переменную мгновенно — он планирует ре-рендер; внутри одного обработчика обновления группируются (батчинг). Если новое значение зависит от предыдущего, используйте функциональную форму setX(prev => ...). Тяжёлую инициализацию делайте ленивой.

Подробно:

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

  function handleClick() {
    setCount(count + 1); // count = 0, планирует 1
    setCount(count + 1); // count ВСЁ ЕЩЁ 0 в этом замыкании, планирует 1
    // Итог: count станет 1, а не 2!
  }

  function handleClickFixed() {
    setCount(prev => prev + 1); // 0 → 1
    setCount(prev => prev + 1); // 1 → 2
    // Итог: 2 — функциональная форма видит актуальное предыдущее значение
  }
}

Асинхронность: count — это значение из замыкания текущего рендера, оно не меняется после вызова сеттера. Новое значение доступно только в следующем рендере.

Батчинг: несколько setX в одном обработчике вызовут один ре-рендер, а не несколько.

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

// ❌ Дорогая функция вызывается на КАЖДОМ рендере (результат игнорируется)
const [data] = useState(expensiveInit());

// ✅ Функция вызывается ОДИН раз при монтировании
const [data] = useState(() => expensiveInit());

⚠️ Ловушка: setCount(count + 1) несколько раз подряд — классическая ошибка. Всегда используйте функциональную форму, когда новое значение зависит от старого.

14

Что такое useEffect, зачем массив зависимостей, cleanup и когда он вызывается?

Короткий ответ: useEffect выполняет побочные эффекты (запросы, подписки, ручные DOM-операции) после рендера. Массив зависимостей определяет, когда эффект перезапускается. Функция, возвращаемая из эффекта, — cleanup, она вызывается перед следующим запуском эффекта и при размонтировании.

Подробно:

useEffect(() => {
  // эффект
  const sub = api.subscribe(id, setData);
  return () => sub.unsubscribe(); // cleanup
}, [id]); // зависимости

Когда вызывается (в зависимости от массива зависимостей):

useEffect(fn);          // после КАЖДОГО рендера
useEffect(fn, []);      // один раз после монтирования (cleanup при размонтировании)
useEffect(fn, [a, b]);  // после монтирования + когда a или b изменились

Порядок при обновлении зависимостей: cleanup предыдущего эффекта → новый эффект.

Эмуляция lifecycle классов:

  • componentDidMountuseEffect(fn, [])
  • componentDidUpdateuseEffect(fn, [deps])
  • componentWillUnmountreturn () => {} из useEffect(fn, [])

⚠️ Ловушка: useEffect НЕ нужен для производных данных. Если значение можно вычислить из существующих state/props во время рендера — вычисляйте прямо там, не дублируйте в state через эффект.

// ❌ Лишний эффект и лишний ре-рендер
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(first + ' ' + last), [first, last]);

// ✅ Просто вычислить при рендере
const fullName = first + ' ' + last;
15

Какие типичные баги связаны с useEffect?

Короткий ответ: Три классических: бесконечный цикл (эффект меняет свою зависимость), пропущенные зависимости (stale-данные), и stale closure (эффект захватил устаревшее значение из старого рендера).

Подробно:

1. Бесконечный цикл:

// ❌ Эффект меняет data, data в зависимостях → эффект снова → бесконечно
useEffect(() => {
  setData([...data, newItem]);
}, [data]);

// ✅ Функциональное обновление + убрать data из зависимостей
useEffect(() => {
  setData(prev => [...prev, newItem]);
}, [newItem]);

2. Пропущенные зависимости:

// ❌ userId не в зависимостях — при смене userId данные не перезапросятся
useEffect(() => { fetchUser(userId); }, []);
// ✅
useEffect(() => { fetchUser(userId); }, [userId]);

3. Stale closure:

// ❌ interval захватил count=0 навсегда (пустой массив deps)
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []); // count всегда 0 внутри
// ✅ функциональная форма не зависит от захваченного count
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

⚠️ Ловушка: Не «глушите» линтер пустым массивом зависимостей, чтобы убрать предупреждение. Либо честно добавьте зависимость, либо примените функциональное обновление / useRef. Игнорирование линтера почти всегда прячет баг.

16

Чем useEffect отличается от useLayoutEffect?

Короткий ответ: useEffect выполняется асинхронно после того, как браузер отрисовал кадр (не блокирует paint). useLayoutEffect выполняется синхронно после мутаций DOM, но до отрисовки — используйте его, когда нужно измерить/изменить DOM до того, как пользователь увидит кадр (иначе будет мерцание).

Подробно:

// useLayoutEffect: измеряем DOM и корректируем до paint — без мерцания
useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
  setTooltipPos(height); // пользователь не увидит промежуточное состояние
}, []);

Порядок: мутация DOM → useLayoutEffect (синхронно, блокирует paint) → браузер рисует → useEffect (асинхронно).

⚠️ Ловушка: useLayoutEffect блокирует отрисовку, поэтому тяжёлая работа в нём вредит производительности. По умолчанию используйте useEffect; переходите на useLayoutEffect только при визуальном мерцании из-за измерений DOM. Также useLayoutEffect выдаёт предупреждение при SSR (не выполняется на сервере).

17

Зачем нужен useRef и чем он отличается от state?

Короткий ответ: useRef хранит мутируемое значение в .current, которое сохраняется между рендерами, но его изменение НЕ вызывает ре-рендер. Используется для доступа к DOM-узлам и для хранения значений, не влияющих на отображение (таймеры, предыдущие значения).

Подробно:

function TextInput() {
  const inputRef = useRef(null);            // ссылка на DOM
  const renderCount = useRef(0);            // мутируемое значение
  renderCount.current++;                    // не вызывает ре-рендер
  return <input ref={inputRef} onClick={() => inputRef.current.focus()} />;
}
useState useRef
Изменение → ре-рендер да нет
Значение между рендерами сохраняется сохраняется
Назначение данные для UI DOM-ссылки, скрытые мутируемые значения

⚠️ Ловушка: Не используйте ref для данных, которые должны отображаться — изменение ref не перерисует UI, и на экране останется старое значение. И не читайте/не пишите ref.current во время рендера (только в обработчиках и эффектах) — это нарушает чистоту рендера.

18

Чем отличаются useMemo и useCallback? Когда они нужны, а когда вредны?

Короткий ответ: useMemo кэширует результат вычисления, useCallback кэширует саму функцию (это useMemo(() => fn, deps)). Оба нужны, чтобы сохранять ссылочную стабильность (referential equality) между рендерами — для тяжёлых вычислений и для предотвращения лишних ре-рендеров мемоизированных детей. Преждевременная мемоизация вредна: добавляет накладные расходы и усложняет код.

Подробно:

// useMemo — кэширует значение
const sorted = useMemo(() => bigList.sort(cmp), [bigList]);

// useCallback — кэширует функцию (стабильная ссылка)
const handleClick = useCallback(() => doSomething(id), [id]);

// Эквивалент:
const handleClick = useMemo(() => () => doSomething(id), [id]);

Зачем нужна ссылочная стабильность: при каждом рендере объекты/функции создаются заново ({} !== {}, () => {} !== () => {}). Если такой объект/функция передаётся в React.memo-компонент или в зависимости useEffect, мемоизация ломается / эффект перезапускается.

const Child = React.memo(ChildComp);
// Без useCallback onClick — новая ссылка каждый рендер → memo бесполезен
<Child onClick={useCallback(() => {}, [])} />

Когда РЕАЛЬНО нужно:

  • Тяжёлое вычисление (сортировка/фильтрация больших массивов) — useMemo.
  • Передача функции/объекта в React.memo-ребёнка.
  • Передача в зависимости другого хука.

Когда ВРЕДНО: оборачивать всё подряд. Сама мемоизация стоит памяти и сравнения зависимостей. Для дешёвых вычислений и компонентов без React.memo это лишний код и оверхед без выгоды.

⚠️ Ловушка: useMemo/useCallback — это подсказка, а не гарантия: React может отбросить кэш. И мемоизация бесполезна, если зависимости меняются каждый рендер (например, передаёте в deps объект, который сами создаёте заново).

19

Что делает React.memo и когда он помогает?

Короткий ответ: React.memo — HOC, который запоминает результат рендера компонента и пропускает ре-рендер, если пропсы не изменились (по поверхностному сравнению — shallow compare). Помогает для «тяжёлых» компонентов, которые часто получают одни и те же пропсы.

Подробно:

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return items.map(i => <Row key={i.id} {...i} />);
});

Поверхностное сравнение: React сравнивает каждый пропс через Object.is (по сути ===). Примитивы сравниваются по значению, объекты/массивы/функции — по ссылке.

// memo не сработает: style — новый объект каждый рендер
<ExpensiveList items={items} style={{ color: 'red' }} />
// Нужно вынести: const style = useMemo(() => ({ color: 'red' }), []);

⚠️ Ловушка: React.memo бесполезен (и даже вреден из-за лишнего сравнения), если компонент почти всегда получает новые пропсы (новые объекты/функции/children). Часто его эффект сводят на нет, передавая инлайн-объекты и стрелочные функции. Связка memo + useCallback/useMemo на пропсах работает в паре.

20

Что такое Context API (useContext) и в чём его проблема?

Короткий ответ: Context позволяет передавать данные через дерево компонентов без props drilling. useContext читает значение ближайшего Provider. Главная проблема: при изменении значения контекста перерисовываются ВСЕ потребители, даже если им важна лишь часть данных.

Подробно:

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
function Button() {
  const theme = useContext(ThemeContext); // без drilling
  return <button className={theme}>OK</button>;
}

Проблема ре-рендеров: когда value Provider меняется, все компоненты, вызывающие useContext, ре-рендерятся. Если в value положить большой объект, любое изменение перерисует всех потребителей.

Митигация: разделять контексты по смыслу, мемоизировать value, выносить редко меняющиеся данные отдельно.

⚠️ Ловушка: Context — это механизм передачи (DI), а не управления состоянием. Он не оптимизирует ре-рендеры и не нормализует данные. Для частых обновлений и сложной логики он не заменяет state manager (Redux/Zustand). Также value={{...}} инлайн создаёт новый объект каждый рендер — мемоизируйте.

21

Когда использовать useReducer вместо useState?

Короткий ответ: useReducer подходит, когда логика state сложная: несколько связанных полей, переходы состояний зависят от предыдущего state, или одно действие меняет несколько значений. Он централизует логику обновлений в чистой функции-редьюсере.

Подробно:

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return { ...state, count: 0 };
    default:          throw new Error('unknown action');
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Преимущества: логика обновлений в одном месте, легко тестировать (чистая функция), dispatch стабилен по ссылке (не нужен useCallback).

⚠️ Ловушка: Для простого state (одно булево/число) useReducer — оверинжиниринг. Reducer обязан быть чистым: никаких мутаций аргументов, запросов, Date.now()/Math.random() внутри.

22

Что такое custom hooks и зачем они нужны?

Короткий ответ: Custom hook — это обычная функция, имя которой начинается с use, использующая внутри другие хуки. Они переиспользуют stateful-логику между компонентами без дублирования и без HOC/render props.

Подробно:

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// Переиспользование в любом компоненте
function Modal() {
  const [open, toggleOpen] = useToggle();
  return <button onClick={toggleOpen}>{open ? 'Закрыть' : 'Открыть'}</button>;
}

Правило именования: обязательно префикс use — по нему линтер понимает, что внутри есть хуки, и применяет правила хуков.

⚠️ Ловушка: Custom hook НЕ разделяет state между компонентами — каждый вызов создаёт независимый экземпляр state. Если двум компонентам нужен общий state, поднимите его выше или используйте Context/store. Хуки переиспользуют логику, а не данные.

23

Какой жизненный цикл у классовых компонентов (терминология)?

Короткий ответ: Три фазы — монтирование, обновление, размонтирование. Основные методы: constructor, render, componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate, getDerivedStateFromProps, componentDidCatch.

Подробно:

class C extends React.Component {
  constructor(props) { super(props); this.state = {}; }   // init
  componentDidMount()   { /* после первого рендера: запросы, подписки */ }
  shouldComponentUpdate(nextProps, nextState) { return true; } // оптимизация
  componentDidUpdate(prevProps) { /* после обновления */ }
  componentWillUnmount() { /* очистка: отписки, таймеры */ }
  render() { return <div />; }
}

Соответствие хукам:

  • componentDidMount + componentDidUpdate + componentWillUnmountuseEffect
  • shouldComponentUpdateReact.memo
  • внутреннее состояние this.stateuseState/useReducer

⚠️ Ловушка: useEffect(fn, []) не идентичен componentDidMount: эффект срабатывает после paint (асинхронно), а componentDidMount — синхронно до paint. Точный аналог по времени — useLayoutEffect. Также методы componentWillMount/componentWillReceiveProps устарели (legacy/unsafe).

24

Чем controlled компоненты отличаются от uncontrolled?

Короткий ответ: В controlled-компоненте значение формы хранится в React-state и устанавливается через value + onChange — React «единственный источник правды». В uncontrolled значение живёт в самом DOM, React читает его через ref при необходимости (defaultValue).

Подробно:

// Controlled: React управляет значением
function Controlled() {
  const [val, setVal] = useState('');
  return <input value={val} onChange={e => setVal(e.target.value)} />;
}

// Uncontrolled: DOM хранит значение, читаем через ref
function Uncontrolled() {
  const ref = useRef();
  return (
    <>
      <input defaultValue="" ref={ref} />
      <button onClick={() => console.log(ref.current.value)}>OK</button>
    </>
  );
}

Controlled даёт полный контроль (валидация на лету, форматирование, условное отключение), но требует ре-рендера на каждый ввод. Uncontrolled проще и производительнее для больших форм, но сложнее для динамической валидации.

⚠️ Ловушка: Нельзя смешивать: передать value без onChange сделает поле «только для чтения» и React выдаст предупреждение. Переключение value с undefined на строку (controlled ↔ uncontrolled) — частый источник warning'ов.

25

Что вызывает ре-рендер компонента и как React решает, что перерисовать?

Короткий ответ: Компонент ре-рендерится при изменении его state, при ре-рендере родителя, при изменении значения подписанного контекста и (для классов) при новых props. Ре-рендер означает повторный вызов функции компонента и пересборку VDOM — но это НЕ обязательно приводит к изменению реального DOM.

Подробно:

Триггеры ре-рендера:

  1. Вызов сеттера state (setX) — если новое значение отличается (Object.is).
  2. Ре-рендер родителя — по умолчанию перерисовывает всех детей (если не React.memo).
  3. Изменение value подписанного useContext.
function Parent() {
  const [n, setN] = useState(0);
  return (
    <>
      <button onClick={() => setN(n + 1)}>+</button>
      <Child /> {/* перерисуется при каждом клике, хотя пропсы не менялись */}
    </>
  );
}

Ключевой принцип: ре-рендер ≠ обновление DOM. React перерисовывает (вызывает функцию) компонент, строит новый VDOM, сравнивает со старым, и трогает DOM ТОЛЬКО там, где есть реальные изменения. Поэтому «лишний» ре-рендер не всегда дорог — дорог он, когда дерево большое или вычисления тяжёлые.

⚠️ Ловушка: Распространённое заблуждение — «передача пропса вызывает ре-рендер ребёнка». На самом деле ребёнок перерисовывается потому, что перерисовался родитель (не из-за самих пропсов). React.memo разрывает эту связь, сравнивая пропсы.

26

Как оптимизировать производительность React-приложения?

Короткий ответ: Профилировать сначала, оптимизировать прицельно. Инструменты: React.memo/useMemo/useCallback против лишних ре-рендеров, code splitting через lazy+Suspense, виртуализация длинных списков, корректные key, перенос state вниз/вынос «дорогих» детей, React DevTools Profiler.

Подробно:

1. Уменьшение ре-рендеров: React.memo для дорогих детей, useMemo/useCallback для стабильных пропсов, поднятие/опускание state.

// Перенос state вниз: ввод не перерисовывает тяжёлый список
function Page() {
  return <><SearchBox /><ExpensiveList /></>; // вместо state в Page
}

2. Code splitting (ленивая загрузка):

const Settings = React.lazy(() => import('./Settings'));
<Suspense fallback={<Spinner />}>
  <Settings />
</Suspense>

3. Виртуализация списков (react-window/react-virtualized): рендерить только видимые элементы из тысяч.

4. Профайлер (React DevTools Profiler): найти, что и почему ре-рендерится, замерить время коммитов.

⚠️ Ловушка: Сначала измерь, потом оптимизируй. Преждевременное обвешивание всего useMemo/memo усложняет код и часто замедляет его (оверхед сравнений). Большинство ре-рендеров дёшевы. Профайлер показывает реальные узкие места.

27

Когда хватает useState/Context, а когда нужен Redux/Zustand/MobX?

Короткий ответ: Локального state (useState/useReducer) хватает для состояния отдельного компонента. Context — для редко меняющихся глобальных данных (тема, локаль, текущий юзер). Внешний store (Redux/Zustand/MobX) нужен при сложном, часто обновляемом глобальном state с множеством потребителей, отладкой и предсказуемостью.

Подробно:

Решение Когда
useState/useReducer состояние одного компонента/поддерева
Context редко меняющиеся глобальные данные (theme, auth, i18n)
Zustand/Jotai глобальный state с минимумом шаблона, точечные подписки
Redux (Toolkit) большое приложение, строгая предсказуемость, DevTools, middleware
MobX реактивный observable-подход, меньше бойлерплейта
React Query/RTK Query серверное состояние (кэш данных API)
// Zustand — минимум шаблона, подписка только на нужный кусок
const useStore = create(set => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) }));
const count = useStore(s => s.count); // ре-рендер только при смене count

⚠️ Ловушка: Главное разграничение — client state vs server state. Данные с сервера (списки, профили) — это кэш с фоновой синхронизацией, инвалидацией, статусами загрузки. Их лучше держать не в Redux вручную, а в React Query/RTK Query. Не складывайте всё подряд в глобальный store.

28

Объясните концепции Redux и Redux Toolkit.

Короткий ответ: Redux — предсказуемый контейнер состояния: единый store, изменения только через dispatch(action), новое состояние вычисляется чистыми reducer-функциями иммутабельно. Redux Toolkit (RTK) — официальный набор, убирающий бойлерплейт (createSlice, встроенный Immer, настроенный store).

Подробно:

Основные понятия:

  • Store — единое дерево состояния (single source of truth).
  • Action — объект { type, payload }, описывающий «что произошло».
  • Reducer — чистая функция (state, action) => newState, без мутаций и сайд-эффектов.
  • Dispatch — отправка action в store, единственный способ изменить state.
// Классический Redux
function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INC': return { ...state, count: state.count + 1 }; // immutable
    default: return state;
  }
}
// Redux Toolkit — то же без бойлерплейта (Immer позволяет «мутировать» черновик)
const slice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: { inc: (state) => { state.count += 1; } }, // Immer → иммутабельно под капотом
});
export const { inc } = slice.actions;

Поток: UI dispatch(action) → reducer вычисляет новое состояние → подписчики (компоненты) ре-рендерятся.

⚠️ Ловушка: Reducer обязан быть чистым и иммутабельным — никаких state.x = ... (в обычном Redux), запросов, мутаций. В RTK «мутация» разрешена синтаксически, но это Immer создаёт новый неизменяемый объект под капотом. Сайд-эффекты (запросы) — в middleware (thunk/saga), не в reducer.

29

Что такое RTK Query / React Query и в чём разница client state vs server state?

Короткий ответ: RTK Query и React Query (TanStack Query) — библиотеки для управления серверным состоянием: кэширование ответов API, дедупликация запросов, фоновое обновление, инвалидация, статусы loading/error. Server state отличается от client state тем, что им владеет сервер, он асинхронный и может устареть.

Подробно:

// React Query — кэш, статусы, рефетч из коробки
function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
  if (isLoading) return <Spinner />;
  return <List items={data} />;
}

Client state (тема, открыт ли модал, форма) — синхронный, целиком наш. Server state (данные API) — асинхронный, разделяется между клиентами, нужен кэш и синхронизация.

⚠️ Ловушка: Хранить серверные данные в Redux вручную (useEffect + dispatch) — это переизобретение кэша: придётся самим писать загрузку, инвалидацию, дедупликацию, обработку гонок. Специализированные библиотеки делают это надёжнее. Redux/Zustand оставляйте для client state.

30

Что такое проблема stale closure в хуках?

Короткий ответ: Stale closure — ситуация, когда функция (в эффекте, колбэке, таймере) «запомнила» значения переменных из старого рендера и продолжает использовать устаревшие данные. Возникает из-за того, что каждый рендер создаёт новое замыкание со своими копиями state/props.

Подробно:

function Timer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ВСЕГДА 0 — замыкание из первого рендера
    }, 1000);
    return () => clearInterval(id);
  }, []); // пустой массив → callback видит count только начального рендера
}

Решения:

  • Функциональное обновление: setCount(c => c + 1) — не зависит от захваченного count.
  • Добавить значение в зависимости (эффект пересоздастся с актуальным замыканием).
  • useRef для хранения всегда-актуального значения: countRef.current.
const countRef = useRef(count);
countRef.current = count; // обновляем каждый рендер
// внутри интервала читаем countRef.current — всегда свежее

⚠️ Ловушка: Stale closure особенно коварен с пустым массивом зависимостей и долгоживущими колбэками (интервалы, обработчики событий, подписки). Линтер зависимостей помогает обнаружить захват устаревших значений.

31

Что такое automatic batching в React 18?

Короткий ответ: Batching — группировка нескольких обновлений state в один ре-рендер. До React 18 батчинг работал только внутри React-обработчиков событий. В React 18 он стал автоматическим везде — в промисах, setTimeout, нативных обработчиках, async-функциях.

Подробно:

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 17 и 18: ОДИН ре-рендер (батчинг в обработчике)
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 17: ДВА ре-рендера; React 18: ОДИН (automatic batching)
}, 1000);

Если нужно принудительно применить обновление синхронно вне батча — есть flushSync (использовать редко, ломает оптимизацию).

⚠️ Ловушка: Automatic batching включается при использовании createRoot (новый API React 18). Если приложение монтируется старым ReactDOM.render, поведение остаётся как в React 17. Большинство кода батчинг не ломает, но если вы полагались на промежуточный ре-рендер между двумя setX в setTimeout — поведение изменится.

32

Что такое concurrent features (Suspense, useTransition)?

Короткий ответ: Concurrent-возможности React 18 позволяют React прерывать и приоритизировать рендеринг. useTransition помечает обновления как несрочные (не блокируют ввод). Suspense объявляет границу с fallback, пока компонент «ждёт» (ленивый код или данные).

Подробно:

// useTransition: тяжёлая фильтрация не подвешивает поле ввода
function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function onChange(e) {
    setQuery(e.target.value);                 // срочно: ввод откликается мгновенно
    startTransition(() => {                    // несрочно: можно прервать
      setResults(filterHugeList(e.target.value));
    });
  }
  return <>{isPending && <Spinner />}<input value={query} onChange={onChange} /></>;
}
// Suspense: показать fallback пока грузится ленивый компонент или данные
<Suspense fallback={<Spinner />}>
  <LazyDashboard />
</Suspense>

useDeferredValue — родственный хук: откладывает обновление значения, чтобы не блокировать срочный рендер.

⚠️ Ловушка: Concurrent-фичи не делают код «магически быстрым» — они меняют приоритеты рендеринга, давая отзывчивость UI. Suspense для получения данных полноценно работает с поддерживающими его фреймворками/библиотеками (Next.js, React Query, RSC), а не с любым fetch в useEffect.

33

Что такое error boundaries?

Короткий ответ: Error boundary — компонент (только классовый), который ловит JS-ошибки рендера в своём поддереве, логирует их и показывает запасной UI вместо краша всего приложения. Реализуется через getDerivedStateFromError и componentDidCatch.

Подробно:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(error, info) { logError(error, info); }
  render() {
    return this.state.hasError ? <h1>Что-то пошло не так</h1> : this.props.children;
  }
}
// Использование
<ErrorBoundary><RiskyComponent /></ErrorBoundary>

⚠️ Ловушка: Error boundary НЕ ловит ошибки: в обработчиках событий (там используйте try/catch), в асинхронном коде (setTimeout, промисы), в SSR и ошибки в самом boundary. Хук-эквивалента нет — это единственный кейс, где до сих пор нужен классовый компонент (или библиотека react-error-boundary).

34

Что такое Portals и Fragments?

Короткий ответ: Portal рендерит дочерние элементы в DOM-узел вне иерархии родителя (для модалок, тултипов, попапов), сохраняя при этом React-контекст и всплытие событий. Fragment (<>...</>) группирует элементы без лишнего DOM-обёртки.

Подробно:

// Portal: модалка рендерится в body, но логически — ребёнок компонента
function Modal({ children }) {
  return ReactDOM.createPortal(children, document.getElementById('modal-root'));
}

// Fragment: вернуть несколько элементов без лишнего <div>
function List() {
  return (
    <>
      <li>Один</li>
      <li>Два</li>
    </>
  );
}

Зачем portal: обойти overflow: hidden/z-index родителя для оверлеев. Важно: события всплывают по React-дереву (по родителю-портал-владельцу), а не по DOM-дереву.

⚠️ Ловушка: Fragment с ключом пишется полной формой <React.Fragment key={id}>, короткий <> не принимает атрибуты. Portal сохраняет React-контекст и всплытие событий по React-дереву — это удобно, но иногда удивляет (клик в портал-модалке всплывёт к логическому родителю).

35

Чем отличаются CSR, SSR, SSG, что такое гидратация и при чём тут Next.js?

Короткий ответ: CSR (Client-Side Rendering) — HTML пустой, всё рисует JS в браузере. SSR (Server-Side Rendering) — сервер отдаёт готовый HTML на каждый запрос. SSG (Static Site Generation) — HTML генерируется заранее при сборке. Гидратация — процесс «оживления» серверного HTML на клиенте: React навешивает обработчики и берёт управление. Next.js — фреймворк над React, дающий SSR/SSG/ISR, роутинг и оптимизации из коробки.

Подробно:

Когда рендерится HTML Плюсы Минусы
CSR в браузере простота, дёшево плохой SEO, медленный first paint
SSR на сервере, на каждый запрос SEO, быстрый контент, свежие данные нагрузка на сервер
SSG при сборке максимально быстро, дёшево раздавать данные «застывают» (нужен ISR/rebuild)

Гидратация: сервер прислал готовый HTML → React на клиенте строит VDOM по тому же дереву и «пристёгивает» обработчики событий. До гидратации страница видна, но не интерактивна.

// Next.js App Router: серверные компоненты по умолчанию,
// 'use client' помечает клиентский (интерактивный) компонент
'use client';
export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

⚠️ Ловушка: Hydration mismatch — если серверный HTML отличается от клиентского первого рендера (например, использовали Date.now(), window, случайные значения, локаль), React выдаёт ошибку и может перерисовать. Поэтому первый клиентский рендер должен совпадать с серверным.

36

Почему важна иммутабельность (immutability) в React?

Короткий ответ: React определяет изменения через поверхностное сравнение по ссылке (Object.is). Если мутировать state напрямую, ссылка не меняется — React не «увидит» изменение и не перерисует (либо перерисует неправильно). Иммутабельность (создание новых объектов/массивов) делает изменения детектируемыми и предсказуемыми.

Подробно:

// ❌ Мутация — ссылка та же, React не увидит изменения
const [user, setUser] = useState({ name: 'Аня', age: 20 });
user.age = 21;
setUser(user); // та же ссылка → ре-рендера может не быть

// ✅ Новый объект
setUser({ ...user, age: 21 });

// Массивы:
setItems([...items, newItem]);          // не push
setItems(items.filter(i => i.id !== x)); // не splice
setItems(items.map(i => i.id === x ? { ...i, done: true } : i));

Иммутабельность также включает корректную работу React.memo/useMemo/PureComponent (они сравнивают по ссылке) и упрощает отладку (time-travel в Redux, понятная история состояний).

⚠️ Ловушка: Поверхностное копирование ({...obj}) не клонирует вложенные объекты — для обновления глубоко вложенного поля нужно копировать всю цепочку до него (или использовать Immer / useImmer). Мутация вложенного объекта внутри «скопированного» родителя — частый скрытый баг.

37

Чем React отличается от Vue и Angular (кратко)?

Короткий ответ: React — минималистичная библиотека для View с JSX и явным управлением обновлениями, экосистема собирается из сторонних пакетов. Vue — прогрессивный фреймворк с шаблонами и встроенной реактивностью, мягкая кривая входа. Angular — полноценный фреймворк (TypeScript, DI, роутинг, формы, RxJS) с жёсткой структурой.

Подробно:

React Vue Angular
Тип библиотека (View) прогрессивный фреймворк полный фреймворк
Шаблоны JSX (JS) HTML-шаблоны + директивы HTML-шаблоны + декораторы
Реактивность явная (setState/хуки) автоматическая (proxy) RxJS / signals + zone.js
Экосистема сторонняя, гибкая официальная (Router, Pinia) всё «из коробки»
Кривая входа средняя низкая высокая

⚠️ Ловушка: Главное концептуальное отличие: в React реактивность явная — вы сами вызываете сеттер, React перерисовывает «снизу». Vue отслеживает зависимости автоматически (реактивные прокси), и компонент знает, какие данные он читает. Поэтому в Vue реже нужна ручная мемоизация.

38

Зачем нужен Virtual DOM?

Короткий ответ: Чтобы дать декларативную модель (описываем UI как функцию от state) без ручных и неэффективных манипуляций реальным DOM. VDOM позволяет React вычислить минимальный набор изменений и применить их пакетно.

Подробно: Ценность VDOM не столько в скорости, сколько в абстракции: разработчик описывает желаемый результат, а согласование старого и нового деревьев и точечное обновление DOM берёт на себя React. Это убирает целый класс багов рассинхронизации UI и данных.

⚠️ Ловушка: Не утверждайте, что «VDOM быстрее DOM». Корректно: VDOM делает обновления предсказуемыми и достаточно быстрыми, избавляя от ручной оптимизации. Существуют подходы и без VDOM (Svelte компилирует в прямые DOM-операции, SolidJS — fine-grained реактивность).

39

Зачем нужны хуки и чем были плохи классы?

Короткий ответ: Хуки дают переиспользование stateful-логики (custom hooks вместо HOC/render props), убирают путаницу с this, группируют связанную логику вместе. Классы страдали от «wrapper hell», размазанной по lifecycle-методам логики и сложности с this.

Подробно: В классах одна фича (например, подписка) разбивалась на componentDidMount + componentWillUnmount, а в одном методе смешивалась несвязанная логика. Хуки (useEffect с cleanup) позволяют держать связанный код рядом и выносить его в переиспользуемые useX.

⚠️ Ловушка: Хуки не «отменяют» классы и не делают код автоматически лучше. Неправильное использование (огромный useEffect, нарушение правил, забытые зависимости) рождает новые баги. Важно понимать модель «порядок вызова хуков».

40

Почему нельзя мутировать state напрямую?

Короткий ответ: Потому что React сравнивает state по ссылке. Мутация не меняет ссылку — React не обнаружит изменение и не перерисует UI (или перерисует неконсистентно), а оптимизации (memo, PureComponent) сломаются.

Подробно: Прямое присваивание state.x = ... не вызывает сеттер, поэтому React не планирует новый рендер. Оно также меняет объект, который принадлежит уже завершённому рендеру: старые closures и memoized-компоненты внезапно видят изменённые данные под прежней ссылкой. Создавайте новый объект или массив и передавайте его сеттеру — новая ссылка делает изменение наблюдаемым и сохраняет модель state как неизменяемого снимка.

⚠️ Ловушка: Даже если после мутации вызвать setState с тем же объектом, ре-рендер может не произойти (та же ссылка). А React.StrictMode в dev помогает выявлять небезопасные мутации, дважды вызывая рендер/эффекты.

41

Когда useMemo вредит?

Короткий ответ: Когда вычисление дешёвое, компонент не обёрнут в React.memo, или зависимости всё равно меняются каждый рендер. Тогда мемоизация только добавляет память и сравнение зависимостей, усложняет код и не даёт выигрыша — это преждевременная оптимизация.

Подробно: useMemo сам стоит ресурсов: хранение кэша + сравнение массива зависимостей на каждом рендере. Если вычисление — это a + b, оборачивать его в useMemo дороже, чем просто посчитать. Выгода появляется только для действительно тяжёлых вычислений или для сохранения ссылочной стабильности, которая кому-то реально нужна (memo-ребёнок, deps хука).

⚠️ Ловушка: Обвешивание всего подряд useMemo/useCallback «на всякий случай» — частый антипаттерн. Сначала профайлер, потом точечная оптимизация. Исключение — новый React Compiler (если включён) автоматически мемоизирует, делая ручные обёртки лишними.

42

Что значит «React — это функция от состояния к UI»?

Короткий ответ: Формула UI = f(state): при одном и том же state компонент всегда возвращает одно и то же описание UI. React, получив новый state, заново вызывает f и согласует результат с экраном. Рендер должен быть чистым.

Подробно: Это ментальная модель React: компонент — чистая функция, преобразующая входные данные (props + state) в дерево элементов. Не нужно думать «как изменить DOM из A в B» — нужно описать, как выглядит UI для текущего state, а переход обеспечит React.

// Один и тот же state → один и тот же вывод (чистота)
function View({ user }) {
  return <h1>{user ? `Привет, ${user.name}` : 'Войдите'}</h1>;
}

⚠️ Ловушка: Из «функции от состояния» следует требование чистоты render-фазы: никаких сайд-эффектов, мутаций пропсов/state, запросов прямо в теле компонента. Всё это — в обработчиках событий и эффектах. StrictMode дважды вызывает render в dev, чтобы выявить нечистоту.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS