Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
42 подробных ответа
01Что такое React и какую проблему он решает?
junior
Короткий ответ: 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 декларативный»?
junior
Короткий ответ: Вы описываете желаемый конечный результат (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?
junior
Короткий ответ: Компонент — это самодостаточный кусок 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 и как он транспилируется?
middle
Короткий ответ: 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). class→className,for→htmlFor(т.к.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 и зачем он нужен?
middle
Короткий ответ: 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 (алгоритм согласования)?
senior
Короткий ответ: Reconciliation — процесс сравнения нового дерева VDOM со старым для определения минимальных изменений. React использует эвристический O(n) алгоритм, опираясь на два допущения: элементы разных типов дают разные деревья, и стабильность узлов в списках задаётся через key.
Подробно:
Точное сравнение двух деревьев — задача O(n³). React сводит её к O(n) за счёт эвристик:
- Разный тип элемента → полная замена поддерева. Если
<div>стал<span>, React уничтожит старый узел со всем содержимым (включая state дочерних компонентов) и создаст новый.
// При смене isLoggedIn React НЕ переиспользует старый компонент:
{isLoggedIn ? <AdminPanel /> : <LoginForm />}
// AdminPanel и LoginForm — разные типы, поддерево пересоздаётся
-
Один тип элемента → обновление пропсов. React оставляет узел, меняет только изменившиеся атрибуты, рекурсивно сравнивает детей.
-
Списки сравниваются по
key. Без key React сопоставляет детей по позиции, что приводит к багам при вставке/удалении в начало.
⚠️ Ловушка: Условный рендеринг компонента одного типа в разных ветках с РАЗНОЙ структурой может неожиданно сохранить state. Например, два <input> в обеих ветках тернарника на одной позиции переиспользуют DOM-узел и сохраняют введённый текст. Решается разными key.
07Зачем нужен `key` и почему не стоит использовать `index` массива?
middle
Короткий ответ: 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 архитектура (кратко)?
senior
Короткий ответ: 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 классовые компоненты — в чём разница и почему перешли на функциональные?
junior
Короткий ответ: Классовые — 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?
junior
Короткий ответ: 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?
middle
Короткий ответ: Данные в 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Зачем появились хуки и каковы правила их использования?
middle
Короткий ответ: Хуки (React 16.8) позволяют использовать state и другие возможности React в функциональных компонентах и переиспользовать логику без классов и HOC. Два правила: вызывать хуки только на верхнем уровне (не в условиях/циклах/вложенных функциях) и только из React-компонентов или других хуков.
Подробно:
Хуки убрали необходимость связывать this в классах, позволили переиспользовать stateful-логику через custom hooks вместо render-props/HOC-«матрёшек» и дали возможность держать рядом код одного эффекта, а не разносить его между lifecycle-методами.
Правила хуков:
- Только на верхнем уровне. Нельзя вызывать хуки внутри условий, циклов, вложенных функций.
// ❌ НЕЛЬЗЯ
if (cond) { const [x, setX] = useState(0); }
// ✅ Правильно
const [x, setX] = useState(0);
if (cond) { /* используем x */ }
- Только из 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 и функциональное обновление?
middle
Короткий ответ: 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 и когда он вызывается?
middle
Короткий ответ: 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 классов:
componentDidMount→useEffect(fn, [])componentDidUpdate→useEffect(fn, [deps])componentWillUnmount→return () => {}изuseEffect(fn, [])
⚠️ Ловушка: useEffect НЕ нужен для производных данных. Если значение можно вычислить из существующих state/props во время рендера — вычисляйте прямо там, не дублируйте в state через эффект.
// ❌ Лишний эффект и лишний ре-рендер
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(first + ' ' + last), [first, last]);
// ✅ Просто вычислить при рендере
const fullName = first + ' ' + last;
15Какие типичные баги связаны с useEffect?
senior
Короткий ответ: Три классических: бесконечный цикл (эффект меняет свою зависимость), пропущенные зависимости (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?
senior
Короткий ответ: 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?
middle
Короткий ответ: 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? Когда они нужны, а когда вредны?
senior
Короткий ответ: 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 и когда он помогает?
middle
Короткий ответ: 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) и в чём его проблема?
middle
Короткий ответ: 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?
middle
Короткий ответ: 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 и зачем они нужны?
middle
Короткий ответ: 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Какой жизненный цикл у классовых компонентов (терминология)?
junior
Короткий ответ: Три фазы — монтирование, обновление, размонтирование. Основные методы: 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+componentWillUnmount≈useEffectshouldComponentUpdate≈React.memo- внутреннее состояние
this.state≈useState/useReducer
⚠️ Ловушка: useEffect(fn, []) не идентичен componentDidMount: эффект срабатывает после paint (асинхронно), а componentDidMount — синхронно до paint. Точный аналог по времени — useLayoutEffect. Также методы componentWillMount/componentWillReceiveProps устарели (legacy/unsafe).
24Чем controlled компоненты отличаются от uncontrolled?
middle
Короткий ответ: В 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 решает, что перерисовать?
middle
Короткий ответ: Компонент ре-рендерится при изменении его state, при ре-рендере родителя, при изменении значения подписанного контекста и (для классов) при новых props. Ре-рендер означает повторный вызов функции компонента и пересборку VDOM — но это НЕ обязательно приводит к изменению реального DOM.
Подробно:
Триггеры ре-рендера:
- Вызов сеттера state (
setX) — если новое значение отличается (Object.is). - Ре-рендер родителя — по умолчанию перерисовывает всех детей (если не
React.memo). - Изменение
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-приложения?
senior
Короткий ответ: Профилировать сначала, оптимизировать прицельно. Инструменты: 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?
senior
Короткий ответ: Локального 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.
middle
Короткий ответ: 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?
senior
Короткий ответ: 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 в хуках?
senior
Короткий ответ: 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?
middle
Короткий ответ: 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)?
senior
Короткий ответ: 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?
middle
Короткий ответ: 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?
middle
Короткий ответ: 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?
senior
Короткий ответ: 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?
senior
Короткий ответ: 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 (кратко)?
middle
Короткий ответ: 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?
concept
Короткий ответ: Чтобы дать декларативную модель (описываем UI как функцию от state) без ручных и неэффективных манипуляций реальным DOM. VDOM позволяет React вычислить минимальный набор изменений и применить их пакетно.
Подробно: Ценность VDOM не столько в скорости, сколько в абстракции: разработчик описывает желаемый результат, а согласование старого и нового деревьев и точечное обновление DOM берёт на себя React. Это убирает целый класс багов рассинхронизации UI и данных.
⚠️ Ловушка: Не утверждайте, что «VDOM быстрее DOM». Корректно: VDOM делает обновления предсказуемыми и достаточно быстрыми, избавляя от ручной оптимизации. Существуют подходы и без VDOM (Svelte компилирует в прямые DOM-операции, SolidJS — fine-grained реактивность).
39Зачем нужны хуки и чем были плохи классы?
concept
Короткий ответ: Хуки дают переиспользование stateful-логики (custom hooks вместо HOC/render props), убирают путаницу с this, группируют связанную логику вместе. Классы страдали от «wrapper hell», размазанной по lifecycle-методам логики и сложности с this.
Подробно: В классах одна фича (например, подписка) разбивалась на componentDidMount + componentWillUnmount, а в одном методе смешивалась несвязанная логика. Хуки (useEffect с cleanup) позволяют держать связанный код рядом и выносить его в переиспользуемые useX.
⚠️ Ловушка: Хуки не «отменяют» классы и не делают код автоматически лучше. Неправильное использование (огромный useEffect, нарушение правил, забытые зависимости) рождает новые баги. Важно понимать модель «порядок вызова хуков».
40Почему нельзя мутировать state напрямую?
concept
Короткий ответ: Потому что React сравнивает state по ссылке. Мутация не меняет ссылку — React не обнаружит изменение и не перерисует UI (или перерисует неконсистентно), а оптимизации (memo, PureComponent) сломаются.
Подробно: Прямое присваивание state.x = ... не вызывает сеттер, поэтому React не планирует новый рендер. Оно также меняет объект, который принадлежит уже завершённому рендеру: старые closures и memoized-компоненты внезапно видят изменённые данные под прежней ссылкой. Создавайте новый объект или массив и передавайте его сеттеру — новая ссылка делает изменение наблюдаемым и сохраняет модель state как неизменяемого снимка.
⚠️ Ловушка: Даже если после мутации вызвать setState с тем же объектом, ре-рендер может не произойти (та же ссылка). А React.StrictMode в dev помогает выявлять небезопасные мутации, дважды вызывая рендер/эффекты.
41Когда useMemo вредит?
concept
Короткий ответ: Когда вычисление дешёвое, компонент не обёрнут в React.memo, или зависимости всё равно меняются каждый рендер. Тогда мемоизация только добавляет память и сравнение зависимостей, усложняет код и не даёт выигрыша — это преждевременная оптимизация.
Подробно: useMemo сам стоит ресурсов: хранение кэша + сравнение массива зависимостей на каждом рендере. Если вычисление — это a + b, оборачивать его в useMemo дороже, чем просто посчитать. Выгода появляется только для действительно тяжёлых вычислений или для сохранения ссылочной стабильности, которая кому-то реально нужна (memo-ребёнок, deps хука).
⚠️ Ловушка: Обвешивание всего подряд useMemo/useCallback «на всякий случай» — частый антипаттерн. Сначала профайлер, потом точечная оптимизация. Исключение — новый React Compiler (если включён) автоматически мемоизирует, делая ручные обёртки лишними.
42Что значит «React — это функция от состояния к UI»?
concept
Короткий ответ: Формула 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, архитектуру и поведенческие истории.