Перейти к основному содержимому

Собеседование JUNIOR FRONTEND REACT разработчика

· 32 мин. чтения

Сегодня мы разберём собеседование с фронтенд-разработчиком Татьяной, которая имеет около двух лет опыта работы и стек на 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, работает с промисами.

Сравнительная таблица

ХарактеристикаCookieslocalStoragesessionStorageIndexedDB
Размер~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 — другие символы игнорируются

Как это работает

  1. Проходим по каждому символу строки.
  2. Если символ — открывающая скобка ((, [, {), помещаем её на вершину стека.
  3. Если символ — закрывающая скобка (), ], }), извлекаем последнюю открывающую скобку из стека и проверяем, что они образуют пару.
  4. Если пара не совпадает или стек пуст (нет открывающей скобки) — последовательность невалидна.
  5. В конце стек должен быть пуст — иначе есть незакрытые скобки.

Сложность

  • Временная: 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 корректно работает с любыми типами ключей, включая объекты, в отличие от обычного объекта, где ключи приводятся к строке.

Сравнение подходов

СпособСложностьСохраняет порядокРаботает с объектами
SetO(n)ДаДа (по ссылке)
filter + indexOfO(n²)ДаДа (по ссылке)
reduce + includesO(n²)ДаДа (по ссылке)
Объект-словарьO(n)ДаНет (ключи — строки)
MapO(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

  1. Создаётся новый пустой объект: {}.
  2. Этот объект связывается с прототипом конструктора: Object.create(User.prototype).
  3. this внутри функции привязывается к новому объекту.
  4. Функция выполняется, наполняя объект свойствами.
  5. Если функция не возвращает явно объект — автоматически возвращается созданный объект.

Обычная функция, возвращающая объект

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()
instanceoftruefalse
ПрототипUser.prototypeObject.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>
);
}

Ключевые различия

ХарактеристикаPropsState
Кто управляетРодительСам компонент
ИзменяемостьТолько чтениеМожно изменять
Источник данныхВнешний (родитель)Внутренний
ПередачаСверху внизЛокально
Вызов ре-рендераКогда родитель передаст новые 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

Ответ собеседника: Правильный. Мемоизация работает по принципу сравнения ссылок. Она нужна для того, чтобы не пересчитывать значения многократно, если они остаются одними и теми же.

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

Ответ затрагивает идею, но сформулирован неточно. Стоит разобрать мемоизацию более структурированно.

Что такое мемоизация

Мемоизация — это техника оптимизации, при которой результаты дорогостоящих вычислений кэшируются и повторно используются при повторных вызовах с теми же входными данными. Вместо повторного вычисления функция возвращает сохранённый результат из кэша.

Принцип работы

  1. При вызове функции проверяем, есть ли результат для данных аргументов в кэше.
  2. Если есть — возвращаем кэшированный результат.
  3. Если нет — вычисляем, сохраняем в кэш и возвращаем.

Простая реализация

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>
);
}

Как это работает

  1. React.createContext(defaultValue) создаёт объект контекста со значением по умолчанию.
  2. Context.Provider оборачивает дерево компонентов и предоставляет значение всем потомкам.
  3. useContext(Context) в любом компоненте-потомке возвращает ближайшее значение Provider'а.
  4. При изменении значения 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), затем оптимизируйте только то, что действительно медленно. Преждевременная оптимизация усложняет код без пользы.