Собеседование JUNIOR FRONTEND REACT разработчика
Сегодня мы разберём собеседование с фронтенд-разработчиком Татьяной, которая имеет около двух лет опыта работы и стек на React и JavaScript. В ходе интервью кандидат продемонстрировала уверенное понимание теоретических основ — от различий между null и undefined до работы с DOM, хранилищами браузера и хуками React, а также успешно справилась с практическими задачами на проверку скобок и удаление дубликатов из массива, хотя и с некоторыми заминками. В целом собеседование прошло в дружелюбной атмосфере, показав, что Татьяна обладает хорошей базой и потенциалом для роста, особенно если будет больше практики в решении алгоритмических задач.
Вопрос 1. В чем отличие null от undefined?
Таймкод: 00:01:24
Ответ собеседника: Правильный. null — это объект, undefined — отдельный тип данных. null означает, что значение есть и оно равно null, а undefined означает, что переменная не определена и значения нет.
Правильный ответ:
Ответ собеседника в целом верный, но стоит уточнить и углубить несколько моментов.
Типы данных
undefined — это отдельный примитивный тип данных в JavaScript. Переменная, которой не присвоено значение, имеет значение undefined по умолчанию. Также undefined возвращается, когда обращаемся к несуществующему свойству объекта или элементу массива.
null — это тоже примитивное значение, но оно относится к типу object (это известный баг языка, который сохраняется для обратной совместимости). null означает намеренное отсутствие значения — разработчик явно указывает, что здесь ничего нет.
Ключевые различия
undefined— значение не было задано (система не знает, что здесь должно быть).null— значение было явно установлено в «пустоту» (разработчик намеренно обнулил).
Проверка типов
typeof undefined // "undefined"
typeof null // "object" (исторический баг)
Сравнение
null == undefined // true (нестрогое сравнение)
null === undefined // false (строгое сравнение — разные типы)
null === null // true
undefined === undefined // true
Практическое применение
undefined используется системой для обозначения неинициализированных переменных, отсутствующих параметров функций, несуществующих свойств.
null используется разработчиком для явного обнуления значения — например, когда нужно очистить ссылку на объект или указать, что значение намеренно отсутствует.
Пример
let a; // a === undefined (не инициализирована)
let b = null; // b === null (явно обнулена)
let obj = {};
obj.x; // undefined (свойство не существует)
obj.x = null; // null (свойство есть, но значение пусто)
Вопрос 2. Как проверить, что поле объекта является массивом?
Таймкод: 00:02:11
Ответ собеседника: Правильный. Использовать специализированный метод Array.isArray().
Правильный ответ:
Ответ верный, но стоит раскрыть тему подробнее, так как существует несколько способов проверки, и не все из них одинаково надёжны.
Array.isArray() — рекомендуемый способ
Это самый надёжный и рекомендуемый метод. Работает корректно во всех средах, включая фреймы и разные контексты выполнения.
Array.isArray([1, 2, 3]) // true
Array.isArray({}) // false
Array.isArray("string") // false
Array.isArray(null) // false
Array.isArray(undefined) // false
Оператор instanceof — ненадёжный способ
Работает в простых случаях, но ломается при работе с несколькими фреймами или контекстами выполнения, так как каждый фрейм имеет свой собственный конструктор Array.
const arr = [1, 2, 3];
arr instanceof Array // true
// Проблема: в другом фрейме или window это не сработает
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr2 = new iframeArray(1, 2, 3);
arr2 instanceof Array; // false — разные конструкторы
Array.isArray(arr2); // true — работает корректно
Проверка через typeof — не подходит
Оператор typeof для массива вернёт "object", что не позволяет отличить массив от обычного объекта.
typeof [1, 2, 3] // "object"
typeof {} // "object" — невозможно различить
Object.prototype.toString — альтернативный способ
Более низкоуровневая проверка, которая тоже работает надёжно в разных контекстах.
Object.prototype.toString.call([1, 2, 3]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Практический пример проверки поля объекта
const obj = {
name: "test",
items: [1, 2, 3],
data: { key: "value" }
};
// Проверка конкретного поля
if (Array.isArray(obj.items)) {
console.log("items — это массив");
}
// Безопасная проверка с защитой от null/undefined
if (Array.isArray(obj?.unknownField)) {
// не упадёт, даже если obj или unknownField не определены
}
Вывод: Array.isArray() — единственный по-настоящему надёжный способ проверки на массив, который корректно работает во всех средах и контекстах выполнения.
Вопрос 3. Что такое DOM-дерево и для чего оно нужно?
Таймкод: 00:02:44
Ответ собеседника: Правильный. DOM-дерево — это граф, описывающий структуру HTML-документа, вложенность элементов и их взаимосвязь. Оно позволяет манипулировать элементами через JavaScript.
Правильный ответ:
Ответ в целом верный, но стоит дополнить деталями о природе DOM, его структуре и практическом применении.
Что такое DOM
DOM (Document Object Model) — это программный интерфейс (API) для HTML и XML документов. Браузер парсит HTML-код и строит древовидную структуру объектов, где каждый узел дерева представляет часть документа. Это не просто граф, а именно дерево — иерархическая структура с одним корневым элементом.
Структура DOM-дерева
Document
└── html
├── head
│ └── title
│ └── "Page Title" (текстовый узел)
└── body
├── div.container
│ ├── h1
│ │ └── "Заголовок" (текстовый узел)
│ └── p
│ └── "Текст" (текстовый узел)
└── script
Каждый HTML-элемент становится узлом-элементом (Element Node), текст внутри элементов — текстовым узлом (Text Node), атрибуты — узлами-атрибутами (Attribute Node), комментарии — узлами-комментариями (Comment Node).
Зачем нужно DOM
- Манипуляция содержимым — изменение текста, атрибутов, стилей элементов через JavaScript.
- Добавление и удаление элементов — динамическое создание нового контента без перезагрузки страницы.
- Обработка событий — подписка на клики, ввод текста, скролл и другие события пользователя.
- Навигация по документу — доступ к родительским, дочерним и соседним элементам.
Примеры работы с DOM
// Получение элемента
const element = document.getElementById('myId');
const items = document.querySelectorAll('.item');
// Изменение содержимого
element.textContent = 'Новый текст';
element.innerHTML = '<span>HTML содержимое</span>';
// Изменение стилей
element.style.color = 'red';
element.classList.add('active');
// Создание и добавление элемента
const newDiv = document.createElement('div');
newDiv.className = 'new-block';
document.body.appendChild(newDiv);
// Удаление элемента
element.remove();
// Навигация
element.parentNode; // родитель
element.children; // дочерние элементы
element.nextSibling; // следующий сосед
DOM vs HTML
Важно понимать, что DOM — это не исходный HTML-код. DOM — это живое объектное представление, которое браузер строит после парсинга HTML. Браузер может автоматически исправлять ошибки в HTML (например, добавлять закрывающие теги), поэтому DOM может отличаться от исходного кода.
Virtual DOM
В современных фреймворках (React, Vue) используется концепция Virtual DOM — лёгкая копия реального DOM в памяти. При изменениях фреймворк сравнивает виртуальное дерево с предыдущей версией (diffing) и обновляет только изменившиеся части реального DOM, что значительно повышает производительность.
Вопрос 4. Какие способы хранения данных в браузере ты знаешь и в чём их отличия?
Таймкод: 00:03:37
Ответ собеседника: Неполный. Упомянуты cookies, localStorage и sessionStorage, но не раскрыты ключевые различия: cookies автоматически отправляются с каждым запросом на сервер и имеют настраиваемое время жизни.
Правильный ответ:
В браузере существует несколько механизмов хранения данных, каждый из которых имеет свои особенности и сценарии применения.
Cookies
- Размер: до ~4 КБ на куку, ограниченное количество на домен (обычно ~50).
- Время жизни: настраивается через
ExpiresилиMax-Age. Если не задано — живут до закрытия браузера (session cookies). - Доступность: автоматически отправляются с каждым HTTP-запросом на сервер (через заголовок
Cookie), доступны и на сервере, и на клиенте. - Область видимости: привязаны к домену и пути (Domain, Path).
- Безопасность: флаги
HttpOnly(недоступны из JavaScript, защита от XSS),Secure(только по HTTPS),SameSite(защита от CSRF).
// Установка cookie
document.cookie = "token=abc123; max-age=3600; path=/; secure; samesite=strict";
// Чтение всех cookies
console.log(document.cookie); // "token=abc123; other=value"
localStorage
- Размер: до ~5-10 МБ (зависит от браузера).
- Время жизни: персистентное — данные сохраняются после закрытия браузера и перезагрузки ОС.
- Доступность: только на клиенте, не отправляется на сервер автоматически.
- Область видимость: привязан к источнику (протокол + домен + порт), общий для всех вкладок одного источника.
- API: синхронное, блокирующее.
localStorage.setItem('user', JSON.stringify({ name: 'John', id: 1 }));
const user = JSON.parse(localStorage.getItem('user'));
localStorage.removeItem('user');
localStorage.clear();
sessionStorage
- Размер: до ~5-10 МБ.
- Время жизни: данные стираются при закрытии вкладки/окна браузера.
- Доступность: только на клиенте, не отправляется на сервер.
- Область видимости: изолирован в рамках одной вкладки. Дублированная вкладка получает копию sessionStorage, но изменения в одной вкладке не видны в другой.
- API: аналогичен localStorage.
sessionStorage.setItem('formDraft', JSON.stringify({ step: 2, data: {} }));
IndexedDB
- Размер: практически не ограничен (зависит от доступного места на диске).
- Время жизни: персистентное.
- Возможности: полноценная NoSQL-база данных в браузере — поддерживает индексы, транзакции, сложные запросы.
- API: асинхронное, событийно-ориентированное, работает с промисами.
const request = indexedDB.open('MyDB', 1);
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: 'John' });
};
Cache API (Service Worker Cache)
- Используется для кэширования HTTP-запросов и ответов.
- Основной инструмент для работы в офлайне (PWA).
- Асинхронный API, работает с промисами.
Сравнительная таблица
| Характеристика | Cookies | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Размер | ~4 КБ | ~5-10 МБ | ~5-10 МБ | Практически без ограничений |
| Отправка на сервер | Автоматически | Нет | Нет | Нет |
| Время жизни | Настраиваемое | Персистентное | До закрытия вкладки | Персистентное |
| Доступ с сервера | Да | Нет | Нет | Нет |
| API | Ручной парсинг | Синхронное | Синхронное | Асинхронное |
| Тип данных | Строка | Строка | Строка | Любой (объекты, файлы) |
Выбор механизма хранения
- Cookies — для токенов аутентификации, данных которые нужны серверу, отслеживания.
- localStorage — для долговременного хранения настроек пользователя, кэша на клиенте.
- sessionStorage — для временных данных формы, состояния вкладки.
- IndexedDB — для больших объёмов структурированных данных, офлайн-приложений.
Вопрос 5. Если объект2 присвоен объекту1, а затем объект1 полностью перезаписан новым объектом, изменится ли объект2?
Таймкод: 00:05:22
Ответ собеседника: Правильный. Объект2 не изменится, так как при полном перезаписывании объект1 создаётся новая область памяти, а объект2 продолжает ссылаться на старую область памяти, на которую изначально ссылался объект1.
Правильный ответ:
Ответ верный. Стоит разобрать это подробнее, так как разница между присваиванием ссылки и мутацией объекта — одна из ключевых тем в JavaScript.
Примитивы vs Ссылки
В JavaScript примитивы (string, number, boolean, null, undefined, symbol, bigint) передаются по значению, а объекты (включая массивы и функции) — по ссылке.
Сценарий из вопроса — переприсваивание ссылки
let obj1 = { name: "Alice" };
let obj2 = obj1; // obj2 теперь указывает на тот же объект в памяти
console.log(obj1 === obj2); // true — одна и та же ссылка
obj1 = { name: "Bob" }; // переприсваивание: obj1 теперь указывает на НОВЫЙ объект
console.log(obj1); // { name: "Bob" }
console.log(obj2); // { name: "Alice" } — не изменился!
console.log(obj1 === obj2); // false — разные объекты
При присваивании obj1 = { name: "Bob" } переменная obj1 начинает указывать на совершенно новый объект в памяти. Переменная obj2 по-прежнему ссылается на исходный объект { name: "Alice" }. Никакого влияния на obj2 это не оказывает.
Важное отличие — мутация объекта
Если вместо переприсваивания изменить свойство объекта (мутация), то изменения будут видны через обе переменные, так как они ссылаются на один и тот же объект:
let obj1 = { name: "Alice" };
let obj2 = obj1;
obj1.name = "Bob"; // мутация — изменяем свойство существующего объекта
console.log(obj1); // { name: "Bob" }
console.log(obj2); // { name: "Bob" } — изменился!
console.log(obj1 === obj2); // true — всё ещё один объект
Визуализация в памяти
// После let obj2 = obj1;
obj1 ──→ { name: "Alice" } ←── obj2
// После obj1 = { name: "Bob" };
obj1 ──→ { name: "Bob" } (новый объект)
obj2 ──→ { name: "Alice" } (старый объект, без изменений)
// После obj1.name = "Bob" (мутация);
obj1 ──→ { name: "Bob" } ←── obj2 (один объект, изменённый)
Практический вывод
- Переприсваивание (
obj1 = newValue) — меняет ссылку переменной, не затрагивая другие переменные. - Мутация (
obj1.prop = value,obj1.push(...)) — изменяет сам объект, и изменения видны всем переменным, ссылающимся на него.
Именно поэтому в функциональном программировании и в тех же React-редьюсерах рекомендуется избегать мутаций и создавать новые объекты вместо изменения существующих.
Вопрос 6. Как обработать потенциальную ошибку в коде?
Таймкод: 00:07:00
Ответ собеседника: Правильный. Обернуть код в конструкцию try...catch, где в блоке try выполняется потенциально опасный код, а в блоке catch обрабатывается ошибка — выводится в консоль или показывается пользователю.
Правильный ответ:
Ответ верный, но тема обработки ошибок в JavaScript значительно шире. Стоит рассмотреть все основные механизмы и паттерны.
try...catch...finally
Базовая конструкция для перехвата синхронных ошибок. Блок finally выполняется всегда, независимо от того, произошла ошибка или нет.
try {
const data = JSON.parse(userInput);
console.log(data);
} catch (error) {
console.error('Ошибка парсинга:', error.message);
// Уведомление пользователя
} finally {
// Выполняется всегда — очистка ресурсов, скрытие лоадера
hideLoadingSpinner();
}
Важно: try...catch не ловит ошибки в асинхронном коде, если только он не обёрнут непосредственно внутри блока try.
// НЕ СРАБОТАЕТ — ошибка произойдёт позже, вне контекста try...catch
try {
setTimeout(() => {
throw new Error('async error');
}, 1000);
} catch (e) {
console.log('Не поймает:', e);
}
// СРАБОТАЕТ — ошибка внутри асинхронной функции с await
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Ошибка загрузки:', error);
throw error; // пробрасываем дальше или обрабатываем
}
}
Обработка ошибок в промисах
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error))
.finally(() => hideLoadingSpinner());
Глобальная обработка ошибок
Для перехвата непойманных ошибок на уровне всего приложения:
// Непойманные исключения
window.onerror = (message, source, line, col, error) => {
logErrorToService({ message, source, line, col, stack: error?.stack });
return true; // предотвращает вывод в консоль по умолчанию
};
// Непойманные rejection промисов
window.addEventListener('unhandledrejection', (event) => {
logErrorToService({ reason: event.reason });
event.preventDefault();
});
Создание собственных типов ошибок
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(status, message) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
// Использование
try {
if (!email) throw new ValidationError('email', 'Email обязателен');
if (!response.ok) throw new NetworkError(response.status, 'Сервер недоступен');
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof NetworkError) {
showNetworkError(error.message);
} else {
showGenericError();
}
}
Паттерны обработки ошибок
Fail-fast — проверяем входные данные сразу и выбрасываем ошибку при невалидных данных:
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Аргументы должны быть числами');
}
if (b === 0) {
throw new RangeError('Деление на ноль');
}
return a / b;
}
Result-паттерн — вместо выбрасывания исключения возвращаем объект с результатом или ошибкой:
function parseJSON(input) {
try {
return { success: true, data: JSON.parse(input) };
} catch (error) {
return { success: false, error: error.message };
}
}
const result = parseJSON('{"key": "value"}');
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
Ключевые принципы
- Не глотайте ошибки молча — пустой
catchблок скрывает проблемы. - Обрабатывайте ошибки на том уровне, где можете что-то с ними сделать.
- Логируйте ошибки для отладки, но показывайте пользователю понятные сообщения.
- Используйте разные типы ошибок для разных ситуаций (валидация, сеть, бизнес-логика).
try...catch— для синхронного кода иasync/await. Для промисов —.catch().
Вопрос 7. Что вернёт функция с блоками try, catch и finally, если каждый из них возвращает разные значения?
Таймкод: 00:08:03
Ответ собеседника: Неправильный. Было высказано сомнение, что finally выполнится, и предположение, что вернётся значение из try. На самом деле блок finally выполняется всегда и его возвращаемое значение перезаписывает значения из try и catch.
Правильный ответ:
Это один из самых коварных нюансов JavaScript, который часто становится вопросом на собеседованиях.
Главное правило
Если блок finally содержит return, то именно его значение будет возвращено функцией. Значения из return в блоках try и catch будут проигнорированы.
Демонстрация
function testFinally() {
try {
return 'from try';
} catch (e) {
return 'from catch';
} finally {
return 'from finally';
}
}
console.log(testFinally()); // "from finally"
Даже если в try есть return, блок finally выполнится после него и перезапишет возвращаемое значение.
Сценарий с ошибкой в catch
function testWithCatch() {
try {
throw new Error('oops');
} catch (e) {
return 'from catch';
} finally {
return 'from finally';
}
}
console.log(testWithCatch()); // "from finally"
Даже когда ошибка перехвачена в catch и там стоит return, finally всё равно перезапишет результат.
Если finally не содержит return
Если в finally нет явного return, то возвращается значение из последнего выполненного блока (try или catch):
function testNoReturn() {
try {
return 'from try';
} finally {
console.log('finally выполнился');
// нет return
}
}
console.log(testNoReturn()); // "from try"
finally с throw
Если finally выбрасывает ошибку, она заменяет любой return из try или catch:
function testThrow() {
try {
return 'from try';
} finally {
throw new Error('from finally');
}
}
testThrow(); // Uncaught Error: from finally
Почему так происходит
Когда в try или catch встречается return, значение сохраняется, но выполнение не завершается сразу. Сначала обязательно выполняется блок finally. Если в finally тоже есть return, он заменяет сохранённое значение. Это часть спецификации ECMAScript — механизм называется «completion records».
Практический вывод
- Не используйте
returnвнутриfinally— это приводит к неожиданному поведению и считается плохой практикой. - Блок
finallyпредназначен для очистки ресурсов (закрытие соединений, снятие блокировок, скрытие лоадеров), а не для возврата значений. - Если вы видите
returnвfinally— это почти наверняка баг или намеренное переопределение, которое стоит задокументировать.
Вопрос 8. Напиши функцию для проверки правильности последовательности скобок в строке.
Таймкод: 00:09:34
Ответ собеседника: Неполный. Предложена идея со стеком и массивом соответствия скобок, но финальное решение для всех типов скобок не доведено до конца.
Правильный ответ:
Это классическая задача на стек. Суть: для каждой открывающей скобки нужно проверить, что есть соответствующая закрывающая скобка в правильном порядке.
Решение
function isValidBrackets(str) {
const stack = [];
const pairs = {
'(': ')',
'[': ']',
'{': '}'
};
const closing = new Set([')', ']', '}']);
for (const char of str) {
if (pairs[char]) {
// Открывающая скобка — кладём в стек
stack.push(char);
} else if (closing.has(char)) {
// Закрывающая скобка — проверяем соответствие
const last = stack.pop();
if (!last || pairs[last] !== char) {
return false;
}
}
// Остальные символы игнорируем
}
// Если стек пуст — все скобки закрыты правильно
return stack.length === 0;
}
Тесты
console.log(isValidBrackets("()")); // true
console.log(isValidBrackets("()[]{}")); // true
console.log(isValidBrackets("{[()]}")); // true
console.log(isValidBrackets("(]")); // false
console.log(isValidBrackets("([)]")); // false
console.log(isValidBrackets("(((")); // false
console.log(isValidBrackets("a + (b * c)")); // true — другие символы игнорируются
Как это работает
- Проходим по каждому символу строки.
- Если символ — открывающая скобка (
(,[,{), помещаем её на вершину стека. - Если символ — закрывающая скобка (
),],}), извлекаем последнюю открывающую скобку из стека и проверяем, что они образуют пару. - Если пара не совпадает или стек пуст (нет открывающей скобки) — последовательность невалидна.
- В конце стек должен быть пуст — иначе есть незакрытые скобки.
Сложность
- Временная: O(n) — один проход по строке.
- Пространственная: O(n) — в худшем случае все символы открывающие скобки.
Альтернативная реализация через Map
function isValidBrackets(str) {
const stack = [];
const map = new Map([
[')', '('],
[']', '['],
['}', '{']
]);
for (const char of str) {
if (!map.has(char)) {
// Открывающая скобка
stack.push(char);
} else {
// Закрывающая скобка
if (stack.pop() !== map.get(char)) {
return false;
}
}
}
return stack.length === 0;
}
В этой версии логика инвертирована: Map хранит соответствие «закрывающая → открывающая», что делает проверку чуть компактнее.
Расширенная версия с отслеживанием позиции ошибки
function isValidBracketsDetailed(str) {
const stack = [];
const pairs = { '(': ')', '[': ']', '{': '}' };
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (pairs[char]) {
stack.push({ char, pos: i });
} else if (Object.values(pairs).includes(char)) {
const last = stack.pop();
if (!last || pairs[last.char] !== char) {
return { valid: false, errorAt: i, reason: `Ожидалась закрывающая для '${last?.char ?? '?'}', найдена '${char}'` };
}
}
}
if (stack.length > 0) {
const unclosed = stack[stack.length - 1];
return { valid: false, errorAt: unclosed.pos, reason: `Незакрытая скобка '${unclosed.char}'` };
}
return { valid: true };
}
Ключевые моменты для собеседования
- Стек — идеальная структура для задач на вложенность и соответствие пар.
- Не забудьте проверить стек на пустоту в конце (незакрытые скобки).
- Не забудьте проверить стек на пустоту при извлечении (лишняя закрывающая скобка).
Вопрос 9. Напиши функцию, которая принимает массив и возвращает массив без повторяющихся элементов (удаляет все дубликаты).
Таймкод: 00:21:26
Ответ собеседния: Неполный. Предложено решение с двумя вложенными циклами O(n²), более оптимальное решение через словарь начато, но не доведено до конца.
Правильный ответ:
Существует несколько способов решения этой задачи, от простых до оптимальных.
Способ 1: Set — самый простой и элегантный
function removeDuplicates(arr) {
return [...new Set(arr]);
}
console.log(removeDuplicates([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]
console.log(removeDuplicates(['a', 'b', 'a', 'c', 'b'])); // ['a', 'b', 'c']
Set хранит только уникальные значения. Оператор распространения ... преобразует Set обратно в массив.
- Временная сложность: O(n)
- Пространственная сложность: O(n)
Способ 2: filter + indexOf
function removeDuplicates(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
Оставляем только первое вхождение каждого элемента. indexOf всегда возвращает индекс первого найденного совпадения.
- Временная сложность: O(n²) —
indexOfпроходит по массиву на каждой итерации. - Пространственная сложность: O(n)
Способ 3: reduce + includes
function removeDuplicates(arr) {
return arr.reduce((acc, item) => {
if (!acc.includes(item)) {
acc.push(item);
}
return acc;
}, []);
}
- Временная сложность: O(n²)
- Пространственная сложность: O(n)
Способ 4: Объект-словарь (Map) — оптимальный для примитивов
function removeDuplicates(arr) {
const seen = {};
const result = [];
for (const item of arr) {
if (!seen[item]) {
seen[item] = true;
result.push(item);
}
}
return result;
}
- Временная сложность: O(n)
- Пространственная сложность: O(n)
Способ 5: Map — для любых типов данных
function removeDuplicates(arr) {
const map = new Map();
const result = [];
for (const item of arr) {
if (!map.has(item)) {
map.set(item, true);
result.push(item);
}
}
return result;
}
Map корректно работает с любыми типами ключей, включая объекты, в отличие от обычного объекта, где ключи приводятся к строке.
Сравнение подходов
| Способ | Сложность | Сохраняет порядок | Работает с объектами |
|---|---|---|---|
| Set | O(n) | Да | Да (по ссылке) |
| filter + indexOf | O(n²) | Да | Да (по ссылке) |
| reduce + includes | O(n²) | Да | Да (по ссылке) |
| Объект-словарь | O(n) | Да | Нет (ключи — строки) |
| Map | O(n) | Да | Да (по ссылке) |
Удаление дубликатов объектов по значению
Если нужно сравнивать объекты по содержимому, а не по ссылке:
function removeDuplicateObjects(arr) {
const seen = new Set();
return arr.filter(obj => {
const key = JSON.stringify(obj);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const objects = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
console.log(removeDuplicateObjects(objects));
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
Рекомендация
Для большинства случаев — [...new Set(arr)]. Это самый читаемый, лаконичный и производительный способ. Для объектов по значению — Set с JSON.stringify в качестве ключа.
Вопрос 10. Что такое функция-конструктор и чем она отличается от обычной функции, возвращающей объект?
Таймкод: 00:32:37
Ответ собеседника: Неполный. Кандидат не была знакома с функциями-конструкторами. Предложено создать обычную функцию, возвращающую объект, но не объяснено отличие от конструктора.
Правильный ответ:
Функция-конструктор — это специальный паттерн создания объектов в JavaScript, который использует оператор new.
Как работает функция-конструктор
function User(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
return `Привет, я ${this.name}`;
};
}
const user1 = new User('Alice', 25);
console.log(user1.name); // "Alice"
console.log(user1.greet()); // "Привет, я Alice"
console.log(user1 instanceof User); // true
Что происходит при вызове new
- Создаётся новый пустой объект:
{}. - Этот объект связывается с прототипом конструктора:
Object.create(User.prototype). thisвнутри функции привязывается к новому объекту.- Функция выполняется, наполняя объект свойствами.
- Если функция не возвращает явно объект — автоматически возвращается созданный объект.
Обычная функция, возвращающая объект
function createUser(name, age) {
return {
name: name,
age: age,
greet: function() {
return `Привет, я ${this.name}`;
}
};
}
const user2 = createUser('Bob', 30);
console.log(user2.name); // "Bob"
console.log(user2.greet()); // "Привет, я Bob"
console.log(user2 instanceof createUser); // false
Ключевые различия
Прототипное наследование
Объект, созданный через конструктор, имеет связь с прототипом конструктора. Объект, возвращённый обычной функцией, имеет прототип Object.prototype.
// Конструктор — методы можно вынести в прототип
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
return `Привет, я ${this.name}`;
};
const u1 = new User('Alice');
const u2 = new User('Bob');
console.log(u1.greet === u2.greet); // true — одна функция в прототипе
// Обычная функция — каждый раз создаётся новая функция
function createUser(name) {
return {
name: name,
greet: function() {
return `Привет, я ${this.name}`;
}
};
}
const c1 = createUser('Alice');
const c2 = createUser('Bob');
console.log(c1.greet === c2.greet); // false — разные функции в каждом объекте
instanceof
new User('Alice') instanceof User; // true
createUser('Bob') instanceof createUser; // false
Память
Конструктор с прототипом эффективнее по памяти — методы хранятся в одном месте (прототипе), а не копируются в каждый экземпляр.
Возврат из конструктора
Если конструктор возвращает объект — он заменит созданный по умолчанию:
function User(name) {
this.name = name;
return { hacked: true }; // вернётся этот объект, а не this
}
const u = new User('Alice');
console.log(u); // { hacked: true } — this был проигнорирован
Если вернуть примитив — он будет проигнорирован, вернётся this:
function User(name) {
this.name = name;
return 42; // примитив игнорируется
}
const u = new User('Alice');
console.log(u); // User { name: "Alice" }
Современная альтернатива — классы
С ES6 функции-конструкторы в основном заменены классами, которые являются синтаксическим сахаром над прототипным наследованием:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Привет, я ${this.name}`;
}
}
const user = new User('Alice', 25);
console.log(user instanceof User); // true
Итого
| Характеристика | Конструктор (new) | Обычная функция (return) |
|---|---|---|
| Вызов | new User() | createUser() |
| instanceof | true | false |
| Прототип | User.prototype | Object.prototype |
| Методы в прототипе | Да | Нет (копируются) |
| Эффективность памяти | Выше | Ниже |
Вопрос 11. Что такое props в React и чем они отличаются от состояния (state)?
Таймкод: 00:35:06
Ответ собеседника: Правильный. Props — это аргументы функции, которые передаются в компонент сверху (от родителя). Состояние (state) хранится внутри компонента. Props нежелательно менять внутри компонента.
Правильный ответ:
Ответ верный, но стоит раскрыть тему глубже, так как разница между props и state — фундаментальная концепция React.
Props (свойства)
Props — это данные, которые передаются в компонент извне, от родительского компонента. Компонент получает их как объект-аргумент и использует для рендеринга. Props доступны только для чтения — компонент не должен их изменять.
// Родитель передаёт props
function Parent() {
return <Greeting name="Alice" age={25} />;
}
// Компонент принимает props
function Greeting({ name, age }) {
return <h1>Привет, {name}! Тебе {age} лет.</h1>;
}
State (состояние)
State — это внутренние данные компонента, которые он сам управляет. State может изменяться в ответ на действия пользователя, ответы сервера и другие события. При изменении state React автоматически перерисовывает компонент.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Счётчик: {count}</p>
<button onClick={() => setCount(count + 1)}>Увеличить</button>
</div>
);
}
Ключевые различия
| Характеристика | Props | State |
|---|---|---|
| Кто управляет | Родитель | Сам компонент |
| Изменяемость | Только чтение | Можно изменять |
| Источник данных | Внешний (родитель) | Внутренний |
| Передача | Сверху вниз | Локально |
| Вызов ре-рендера | Когда родитель передаст новые props | Когда вызван setter |
Однонаправленный поток данных
React придерживается принципа однонаправленного потока данных: данные передаются только сверху вниз через props. Если дочернему компоненту нужно изменить данные родителя, родитель передаёт через props callback-функцию:
function Parent() {
const [text, setText] = useState('');
return (
<div>
<Child value={text} onChange={setText} />
<p>Значение: {text}</p>
</div>
);
}
function Child({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
Здесь text — это state родителя, который передаётся в дочерний компонент как prop. Дочерний компонент не меняет его напрямую, а вызывает callback onChange, который обновляет state в родителе.
Props как зависимости для эффектов
Props часто используются как зависимости в useEffect:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // эффект перезапустится при изменении userId
if (!user) return <p>Загрузка...</p>;
return <h1>{user.name}</h1>;
}
Подъём состояния (Lifting State Up)
Когда нескольким компонентам нужен доступ к одним и тем же данным, state поднимается в ближайшего общего предка и передаётся вниз через props:
function TemperatureCalculator() {
const [celsius, setCelsius] = useState(0);
return (
<div>
<CelsiusInput value={celsius} onChange={setCelsius} />
<FahrenheitDisplay celsius={celsius} />
</div>
);
}
Немутабельность props
Попытка изменить prop напрямую приведёт к ошибке или непредсказуемому поведению:
function BadComponent({ count }) {
count = count + 1; // Неправильно! Props только для чтения
return <p>{count}</p>;
}
Если компоненту нужно модифицировать полученное значение, он должен сохранить его в собственный state:
function GoodComponent({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Вывод
Props — это механизм передачи данных от родителя к потомку, аналогичен аргументам функции. State — это внутреннее управляемое состояние компонента, аналогично локальным переменным. Компонент получает props и решает, как их отобразить. Компонент управляет своим state и перерисовывается при его изменении.
Вопрос 12. С какими React хуками приходилось работать и для чего они нужны?
Таймкод: 00:35:43
Ответ собеседника: Неполный. Упомянуты useState, useEffect, useMemo и useCallback, но не раскрыты другие важные хуки: useRef, useReducer, useContext и другие.
Правильный ответ:
Хуки — это функции, которые позволяют функциональным компонентам использовать состояние, побочные эффекты и другие возможности React без написания классов.
Основные хуки
useState — управление состоянием
Позволяет добавить локальное состояние в функциональный компонент. Возвращает массив из текущего значения и функции для его обновления.
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '', email: '' });
const [items, setItems] = useState([]);
useEffect — побочные эффекты
Выполняет побочные эффекты в функциональном компоненте: загрузка данных, подписки, ручное изменение DOM. Заменяет методы жизненного цикла классовых компонентов.
// componentDidMount + componentWillUnmount
useEffect(() => {
const subscription = eventBus.subscribe(handleEvent);
return () => subscription.unsubscribe(); // cleanup
}, []);
// componentDidUpdate по зависимости
useEffect(() => {
document.title = `Счётчик: ${count}`;
}, [count]);
useRef — хранение мутабельных значений и доступ к DOM
Возвращает мутабельный объект, который сохраняется между рендерами. Не вызывает ре-рендер при изменении. Также используется для прямого доступа к DOM-элементам.
// Доступ к DOM-элементу
function TextInput() {
const inputRef = useRef(null);
const focus = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} />
<button onClick={focus}>Фокус</button>
</>
);
}
// Хранение предыдущего значения
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Хранение таймера без ре-рендера
function Timer() {
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
};
}
useMemo — мемоизация вычислений
Кэширует результат дорогостоящего вычисления и пересчитывает его только при изменении зависимостей.
function ExpensiveComponent({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Фильтрация...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
useCallback — мемоизация функций
Кэширует саму функцию, чтобы она не пересоздавалась при каждом рендере. Важно при передаче колбэков в дочерние компоненты, обёрнутые в React.memo.
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Клик');
}, []); // функция создаётся один раз
return <Child onClick={handleClick} />;
}
const Child = React.memo(function Child({ onClick }) {
console.log('Child render');
return <button onClick={onClick}>Нажми</button>;
});
useReducer — управление сложным состоянием
Альтернатива useState для сложной логики управления состоянием. Принимает редьюсер и начальное состояние.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Счётчик: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
useContext — доступ к контексту
Позволяет читать и подписываться на контекст без оборачивания в Consumer.
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Панель инструментов</div>;
}
Дополнительные хуки
useLayoutEffect — аналог useEffect, но выполняется синхронно после всех изменений DOM, но до отрисовки браузером. Используется для измерений DOM и синхронных мутаций.
useImperativeHandle — кастомизирует значение, которое доступно родительскому компоненту через ref.
useDeferredValue — откладывает обновление части UI для более срочных обновлений (React 18).
useTransition — позволяет пометить обновление состояния как несрочное, чтобы не блокировать UI (React 18).
useId — генерирует уникальный идентификатор, стабильный между рендерами (React 18).
useSyncExternalStore — подписка на внешнее хранилище с поддержкой серверного рендеринга.
Правила хуков
- Вызывайте хуки только на верхнем уровне компонента — не внутри условий, циклов или вложенных функций.
- Вызывайте хуки только из функциональных компонентов или пользовательских хуков.
- Имена пользовательских хуков должны начинаться с
use(например,useFetch,useLocalStorage).
Вопрос 13. Что такое мемоизация и как она работает?
Таймкод: 00:36:27
Ответ собеседника: Правильный. Мемоизация работает по принципу сравнения ссылок. Она нужна для того, чтобы не пересчитывать значения многократно, если они остаются одними и теми же.
Правильный ответ:
Ответ затрагивает идею, но сформулирован неточно. Стоит разобрать мемоизацию более структурированно.
Что такое мемоизация
Мемоизация — это техника оптимизации, при которой результаты дорогостоящих вычислений кэшируются и повторно используются при повторных вызовах с теми же входными данными. Вместо повторного вычисления функция возвращает сохранённый результат из кэша.
Принцип работы
- При вызове функции проверяем, есть ли результат для данных аргументов в кэше.
- Если есть — возвращаем кэшированный результат.
- Если нет — вычисляем, сохраняем в кэш и возвращаем.
Простая реализация
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Из кэша:', key);
return cache.get(key);
}
console.log('Вычисляем:', key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Использование
const factorial = memoize(function(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
});
console.log(factorial(5)); // Вычисляем → 120
console.log(factorial(5)); // Из кэша → 120
console.log(factorial(6)); // Вычисляем (частично) → 720
Мемоизация в React
В React мемоизация применяется на трёх уровнях:
useMemo — мемоизация вычисленных значений
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
Кэширует результат вычисления. Пересчитывает только при изменении зависимостей.
useCallback — мемоизация функций
const handleSubmit = useCallback((data) => {
api.save(data);
}, []);
Кэширует саму функцию. Возвращает ту же ссылку, если зависимости не изменились.
React.memo — мемоизация компонента
const MemoizedComponent = React.memo(function MyComponent({ name, age }) {
return <div>{name}: {age}</div>;
});
Предотвращает ре-рендер компонента, если его props не изменились. React сравнивает предыдущие и новые props поверхностным сравнением (shallow comparison).
Поверхностное сравнение (Shallow Comparison)
Это ключевой момент, который упомянул собеседник. React при мемоизации сравнивает props не по глубокому равенству, а по ссылке:
// Объекты и массивы — сравнение по ссылке
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Alice' };
console.log(obj1 === obj2); // false — разные ссылки
const obj3 = obj1;
console.log(obj1 === obj3); // true — одна ссылка
Именно поэтому важно сохранять стабильные ссылки на объекты и функции при передаче в мемоизированные компоненты — иначе React.memo не даст эффекта.
Проблема без мемоизации
function Parent() {
const [count, setCount] = useState(0);
// Новая ссылка на объект при каждом рендере
const config = { theme: 'dark' };
// Новая ссылка на функцию при каждом рендере
const handleClick = () => console.log('click');
return (
<>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Child config={config} onClick={handleClick} />
</>
);
}
const Child = React.memo(function Child({ config, onClick }) {
console.log('Child render');
return <div>{config.theme}</div>;
});
Здесь Child будет перерисовываться при каждом изменении count, даже несмотря на React.memo, потому что config и handleClick — новые ссылки при каждом рендере.
Решение
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: 'dark' }), []);
const handleClick = useCallback(() => console.log('click'), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Child config={config} onClick={handleClick} />
</>
);
}
Теперь Child не будет перерисовываться при изменении count, так как ссылки на config и handleClick стабильны.
Ограничения мемоизации
- Расход памяти — кэш растёт с количеством уникальных наборов аргументов.
- Только для чистых функций — функций без побочных эффектов, которые при одинаковых входах всегда дают одинаковый результат.
- Не всегда стоит применять — если компонент перерисовывается часто и props почти всегда меняются, мемоизация добавит накладные расходы на сравнение без пользы.
Вопрос 14. Для чего нужен хук useContext?
Таймкод: 00:36:55
Ответ собеседника: Неполный. Кандидат предположила, что useContext позволяет расшаривать состояние между компонентами на разных уровнях вложенности, но не работала с контекстом на практике.
Правильный ответ:
useContext — это хук, который позволяет компоненту подписаться на значение React-контекста без необходимости использовать паттерн Consumer.
Проблема, которую решает useContext
При передаче данных через props в глубоко вложенном дереве компонентов возникает проблема «prop drilling» — данные приходится пробрасывать через множество промежуточных компонентов, которые сами эти данные не используют.
// Без контекста — prop drilling
function App() {
const [theme, setTheme] = useState('dark');
return <Layout theme={theme} setTheme={setTheme} />;
}
function Layout({ theme, setTheme }) {
return <Sidebar theme={theme} setTheme={setTheme} />;
}
function Sidebar({ theme, setTheme }) {
return <ThemeButton theme={theme} setTheme={setTheme} />;
}
function ThemeButton({ theme, setTheme }) {
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Текущая тема: {theme}
</button>;
}
Здесь Layout и Sidebar просто пробрасывают theme и setTheme, не используя их сами.
Решение с useContext
// 1. Создаём контекст
const ThemeContext = React.createContext('light');
// 2. Оборачиваем дерево в Provider
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Промежуточные компоненты ничего не пробрасывают
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <ThemeButton />;
}
// 4. Любой компонент внутри дерева может получить значение
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Текущая тема: {theme}
</button>
);
}
Как это работает
React.createContext(defaultValue)создаёт объект контекста со значением по умолчанию.Context.Providerоборачивает дерево компонентов и предоставляет значение всем потомкам.useContext(Context)в любом компоненте-потомке возвращает ближайшее значение Provider'а.- При изменении значения Provider'а все компоненты, использующие
useContext, перерисовываются.
Несколько контекстов
Можно использовать несколько контекстов одновременно:
const ThemeContext = React.createContext('light');
const UserContext = React.createContext(null);
function App() {
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState({ name: 'Alice' });
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(ThemeContext);
const user = useContext(UserContext);
return <header className={theme}>Привет, {user.name}</header>;
}
Кастомный хук для контекста
Хорошая практика — оборачивать useContext в кастомный хук с проверкой:
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme должен использоваться внутри ThemeProvider');
}
return context;
}
// Использование
function ThemeButton() {
const { theme, setTheme } = useTheme(); // безопасный доступ
return <button className={theme}>Переключить</button>;
}
Типичные сценарии использования
- Тема оформления (светлая/тёмная тема).
- Текущий пользователь (аутентификация, данные профиля).
- Локализация (текущий язык, переводы).
- Настройки приложения (регион, валюта).
Когда не стоит использовать контекст
- Для данных, которые нужны только в двух соседних компонентах — достаточно props.
- Для часто меняющихся данных — каждый контекст вызывает ре-рендер всех подписчиков. В таких случаях лучше использовать внешние хранилища (Redux, Zustand, MobX).
- Контекст — это механизм передачи данных, а не управление состоянием. Он не имеет встроенных инструментов для дебага, middleware, персистентности.
Вопрос 15. Как предотвратить лишний рендер дочернего компонента в React при изменении состояния родителя?
Таймкод: 00:37:31
Ответ собеседника: Правильный. Обернуть дочерний компонент в React.memo(). Это предотвратит повторный рендер компонента, если его props не изменились. Если компонент принимает пропсы, можно также использовать useMemo для мемоизации передаваемых значений.
Правильный ответ:
Ответ верный, но стоит рассмотреть все доступные инструменты и нюансы их применения.
React.memo — мемоизация компонента
Оборачивает компонент и предотвращает его ре-рендер, если props не изменились (поверхностное сравнение).
const Child = React.memo(function Child({ name }) {
console.log('Child render');
return <div>{name}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Счётчик: {count}
</button>
<Child name={name} /> {/* Не перерисуется при изменении count */}
</div>
);
}
Важно: React.memo работает только со стабильными ссылками на props
Если передавать новые объекты, массивы или функции при каждом рендере — React.memo не даст эффекта:
// Плохо — новая ссылка на каждый рендер
function Parent() {
return (
<>
<Child config={{ theme: 'dark' }} /> // новый объект
<Child onClick={() => console.log('click')} /> // новая функция
</>
);
}
useMemo — стабилизация объектов и массивов
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
const [items, setItems] = useState([]);
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return <Child config={config} items={sortedItems} />;
}
useCallback — стабилизация функций
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('click');
}, []); // стабильная ссылка
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Child onClick={handleClick} /> {/* Не перерисуется */}
</div>
);
}
Все способы предотвращения лишних рендеров
| Инструмент | Что мемоизирует | Когда использовать |
|---|---|---|
React.memo | Рендер компонента | Дочерний компонент рендерится дорого, а props редко меняются |
useMemo | Вычисленное значение | Дорогостоящие вычисления, стабилизация объектов/массивов для дочерних компонентов |
useCallback | Ссылку на функцию | Передача колбэков в мемоизированные дочерние компоненты |
| Правильная структура state | Область ре-рендера | Размещать state как можно ближе к компонентам, которые его используют |
key | Идентичность элемента | Сброс состояния компонента при изменении ключа |
Оптимизация через структуру компонентов
Иногда лучшая оптимизация — не использование мемоизации, а правильная архитектура:
// Плохо — всё перерисовывается
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Счётчик: {count}</button>
<ExpensiveChild /> {/* Перерисовывается при изменении count */}
</div>
);
}
// Хорошо — state изолирован
function Parent() {
return (
<div>
<Counter />
<ExpensiveChild /> {/* Не зависит от счётчика */}
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Счётчик: {count}</button>;
}
Паттерн children для изоляции рендеров
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Счётчик: {count}</button>
<StaticContent>
<ExpensiveChild /> {/* children не зависит от count */}
</StaticContent>
</div>
);
}
function StaticContent({ children }) {
return <div className="static">{children}</div>;
}
Когда НЕ стоит мемоизировать
- Компонент рендерится быстро — мемоизация добавит накладные расходы на сравнение props.
- Props почти всегда меняются —
React.memoбудет сравнивать, но всё равно рендерить. - Компонент простой и лёгкий — оптимизация не даст заметного эффекта.
Правило: сначала измерьте производительность (React DevTools Profiler), затем оптимизируйте только то, что действительно медленно. Преждевременная оптимизация усложняет код без пользы.
