МИЛАЯ СОБЕСЕДУЮЩАЯ НА FRONTEND СОБЕСЕДОВАНИИ С ЗП 300К. РЕАЛЬНОЕ ФРОНТЕНД СОБЕСЕДОВАНИЕ 2025!
Сегодня мы разберём техническое собеседование на позицию фронтенд-разработчира, в ходе которого кандидат демонстрирует уверенное владение ключевыми технологиями — HTML, JavaScript, React, TypeScript и принципами SOLID. Интервьюер последовательно проверяет глубину знаний: от базовых концепций вроде iframe, различий между SVG и IMG, особенностей работы с прототипами и контекстом в JavaScript — до продвинутых тем, таких как жизненный цикл компонентов в React, оптимизация через memo, useCallback, useRef, а также нюансы реактивности в MobX и сравнение стейт-менеджеров. Особое внимание уделяется практической части: рефакторинг компонента с учётом best practices, работа с событиями, предотвращение утечек памяти и корректная типизация обобщённых функций в TypeScript. Кандидат показывает как понимание теории, так и способность применять её в реальных задачах, что делает собеседование не просто проверкой знаний, а полноценным диалогом о подходах к разработке.
Вопрос 1. Что такое iframe и для чего он используется?
Таймкод: 00:01:03
Ответ собеседника: правильный. iframe позволяет встроить другую веб-страницу в текущее приложение, а с помощью postMessage можно организовать общение между iframe-ами.
Правильный ответ:
<iframe> (inline frame) — это HTML-элемент, который позволяет встроить одну HTML-страницу внутрь другой. По сути, он создаёт изолированный контекст рендеринга прямо внутри родительской страницы.
Основные характеристики и свойства:
Каждый iframe создаёт собственное окно (Window), собственный документ (Document) и отдельный контекст выполнения JavaScript. Это означает, что стили и скрипты внутри iframe не влияют на родительскую страницу и наоборот. Атрибуты src, width, height, sandbox, allow, loading (lazy loading) и csp позволяют гибко управлять поведением встроенного фрейма.
Типичные сценарии использования:
- Встраивание стороннего контента: видео с YouTube, карты Google Maps, виджеты социальных сетей, рекламные баннеры от рекламных сетей.
- Изоляция компонентов: встраивание независимых виджетов (чат, форма оплаты, платёжные шлюзы), чтобы их стили и скрипты не конфликтовали с основным приложением.
- Микрофронтенды: каждая команда разрабатывает свой модуль как отдельное приложение, которое встраивается через iframe в единый shell.
- Безопасная загрузка пользовательского контента: например, превью HTML-писем или пользовательских шаблонов в изолированной среде.
Коммуникация между iframe и родителем:
Для обмена данными между окнами используется API window.postMessage(). Это безопасный механизм кросс-доменной коммуникации:
<!-- Родительская страница -->
<script>
const iframe = document.getElementById('myFrame');
// Отправка сообщения в iframe
iframe.contentWindow.postMessage({ action: 'update', data: 42 }, 'https://trusted-domain.com');
// Получение сообщения от iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-domain.com') return; // Всегда проверяйте origin!
console.log('Получено:', event.data);
});
</script>
Внутри iframe:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent-domain.com') return;
// Обработка сообщения
window.parent.postMessage({ status: 'ok' }, event.origin);
});
Атрибут sandbox для безопасности:
Атрибут sandbox позволяет ограничить возможности содержимого iframe. Без него iframe имеет полный доступ к формам, скриптам, навигации и т.д.:
<!-- Максимально изолированный iframe -->
<iframe src="https://untrusted.com" sandbox></iframe>
<!-- Разрешаем только скрипты и формы -->
<iframe src="https://untrusted.com" sandbox="allow-scripts allow-forms"></iframe>
Значения sandbox: allow-scripts, allow-forms, allow-same-origin, allow-popups, allow-top-navigation, allow-modals и другие.
Ограничения и проблемы:
- SEO-контент внутри iframe плохо индексируется поисковыми системами.
- Производительность: каждый iframe — это отдельный документ, отдельный event loop, отдельная загрузка ресурсов. Множество iframe создают значительную нагрузку.
- Адаптивность: необходимо вручную управлять размерами, особенно для резинового дизайна. Библиотеки вроде
iframe-resolverрешают эту проблему через postMessage с передачей высоты контента. - Безопасность: clickjacking, кража данных. Защита через
X-Frame-OptionsилиContent-Security-Policy: frame-ancestorsна стороне сервера. - Доступность (a11y): screen readers могут некорректно обрабатывать вложенные фреймы, важно использовать
titleатрибут.
Альтернативы iframe:
Для некоторых задач iframe можно заменить на Web Components, Shadow DOM, AJAX-загрузку контента, или на серверный рендеринг через Edge Side Includes (ESI). Однако для полной изоляции выполнения кода iframe остаётся единственным нативным механизмом в браузере.
Вопрос 2. Какие плюсы и минусы использования iframe?
Таймкод: 00:01:40
Ответ собеседника: неполный. Из плюсов — помогает решать определённые задачи, например, отображение документов для печати. Из минусов — неудобная коммуникация между iframe-ами через postMessage, сложности с управлением состоянием, а также подверженность атакам типа clickjacking.
Правильный ответ:
Плюсы использования iframe:
1. Полная изоляция контекстов
Каждый iframe создаёт собственный Window, Document, ExecutionContext. Это означает, что CSS-стили и JavaScript-переменные родительской страницы и встроенного фрейма никак не пересекаются. Это критически важно при интеграции сторонних компонентов или контента от ненадёжных источников.
2. Безопасность через sandbox
Атрибут sandbox позволяет детально контролировать, какие действия разрешены внутри iframe: выполнение скриптов, отправка форм, навигация по вкладкам, доступ к родительскому DOM. Это единственный нативный механизм в браузере для полной изоляции выполнения кода.
3. Простота интеграции стороннего контента
Встраивание видео с YouTube, карт, виджетов соцсетей, платёжных форм — всё это требует лишь одной строки HTML. Не нужно разбирать API, адаптировать стили, разрешать конфликты.
4. Независимость жизненного цикла
Содержимое iframe загружается и работает автономно. Ошибки внутри iframe не роняют родительское приложение. Это обеспечивает fault isolation — сбой одного виджета не приводит к падению всей страницы.
5. Поддержка кросс-доменного контента
В отличие от AJAX-запросов (ограничены same-origin policy), iframe может загружать контент с любого домена без CORS-ограничений.
6. Удобство для печати и превью
iframe часто используют для рендеринга документов, отчётов, email-шаблонов — изолированный контекст позволяет применять отдельные стили печати без влияния на основное приложение.
Минусы использования iframe:
1. Проблемы с производительностью
Каждый iframe — это отдельный документ с собственным event loop, отдельной загрузкой CSS/JS/шрифтов, отдельным рендерингом. На мобильных устройствах это особенно критично: увеличивается потребление памяти, растёт время загрузки. Множество iframe на одной странице могут значительно замедлить рендеринг.
2. Сложность коммуникации между контекстами
postMessage — это асинхронный текстовый протокол. Нет типобезопасности, нет гарантий доставки, нет встроенного механизма запрос-ответ. Приходится вручную реализовывать маршрутизацию сообщений, обработку таймаутов, валидацию event.origin. Для сложных сценариев это превращается в полноценный messaging-фреймворк.
3. Проблемы с адаптивностью и размерами
iframe не подстраивает свою высоту под содержимое автоматически. Для динамического контента нужно либо использовать iframe-resizer (постоянная передача высоты через postMessage), либо задавать фиксированные размеры, что ломает адаптивность.
4. SEO-проблемы
Поисковые роботы плохо индексируют содержимое iframe. Контент внутри фрейма может не попасть в поисковую выдачу или быть привязан к домену iframe, а не к родительской странице.
5. Ограничения доступности (a11y)
Screen readers могут некорректно обрабатывать вложенные фреймы. Навигация с клавиатуры между iframe и родителем может работать непредсказуемо. Необходимо явно задавать title и ARIA-атрибуты.
6. Сложность отладки
DevTools показывает iframe как отдельный контекст. Нужно переключаться между execution context для отладки. Логи, ошибки, performance-профилирование — всё разнесено по разным контекстам.
7. Проблемы с маршрутизацией и навигацией
История браузера не отражает навигацию внутри iframe автоматически. Кнопка «Назад» может вести себя непредсказуемо. URL в адресной строке не меняется при навигации внутри фрейма.
8. Безопасность с обеих сторон
iframe подвержен clickjacking, а также может сам быть использован для атак. Необходимо настраивать Content-Security-Policy: frame-ancestors, X-Frame-Options, валидировать event.origin при каждом message.
Когда использовать iframe, а когда нет:
iframe оправдан, когда нужна полная изоляция выполнения кода (сторонние виджеты, платёжные формы, ненадёжный контент). Для внутренних компонентов своего приложения лучше использовать Web Components, Shadow DOM или просто компонентную архитектуру без iframe.
Вопрос 3. В чём разница между тегом img, SVG и Base64 для вставки изображений в HTML?
Таймкод: 00:02:54
Ответ собеседника: неполный. img делает запрос на сервер за картинкой (если путь внешний), SVG — это векторная графика без атрибута src, подходящая для иконок и гибко настраиваемая через атрибуты. Base64 — это строковое представление изображения, которое можно встроить прямо в код, не загружая на сервер, но при этом размер строки может быть очень большим (больше мегабайта). Не упомянуто, что img поддерживает локальные файлы без запроса на сервер, а также не раскрыта разница между растровыми и векторными форматами.
Правильный ответ:
Тег <img> — растровая графика через внешний ресурс
Тег <img> — это стандартный способ вставки растровых изображений (PNG, JPEG, WebP, AVIF, GIF) в HTML-документ. Браузер делает отдельный HTTP-запрос для загрузки файла по указанному в src пути:
<img src="/images/photo.webp" alt="Описание" width="400" height="300">
Ключевые особенности:
- Отдельный HTTP-запрос — изображение загружается асинхронно, параллельно с рендерингом страницы. Браузер может кэшировать файл и не загружать его повторно.
- Растровый формат — изображение состоит из пикселей. При масштабировании теряет качество (пикселизация). Размер файла зависит от разрешения.
- Атрибуты
srcsetиsrs— позволяют загружать разные версии изображения в зависимости от плотности пикселей экрана и его ширины (responsive images):
<img srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
src="photo-800.webp" alt="Фото">
- Атрибут
loading="lazy"— откладывает загрузку изображения до момента, когда оно приблизится к viewport, что экономит трафик и ускоряет начальную загрузку страницы. - Атрибут
decoding="async"— декодирование изображения выполняется асинхронно, не блокируя основной поток.
SVG — векторная графика
SVG (Scalable Vector Graphics) — это формат на основе XML для описания векторных изображений. В отличие от растровых форматов, SVG описывает фигуры математически (линии, кривые, круги, полигоны), поэтому масштабируется без потери качества на любых разрешениях.
SVG можно использовать двумя способами:
Через <img>:
<img src="icon.svg" alt="Иконка">
Инлайн (inline) — прямо в HTML:
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 22h20L12 2z" fill="currentColor"/>
</svg>
Ключевые особенности SVG:
- Бесконечное масштабирование — идеально для иконок, логотипов, диаграмм, графиков. Один файл выглядит одинаково на экране 320px и на 4K-мониторе.
- Инлайн SVG доступен для стилизации и анимации — можно менять цвета через CSS (
fill,stroke), применятьtransform,transition,animation, обращаться к отдельным элементам через JavaScript:
.icon:hover path {
fill: #007bff;
transition: fill 0.2s;
}
- Размер файла — для простых иконок SVG-файл обычно занимает от 200 байт до 5 КБ, что сопоставимо или даже меньше, чем PNG аналогичного визуального размера. Но для сложных иллюстраций с градиентами и множеством путей SVG может стать тяжелее растра.
- SEO и доступность — внутри SVG можно использовать
<title>и<desc>, что помогает поисковикам и screen readers. - Инлайн SVG не создаёт дополнительных HTTP-запросов — он является частью DOM-дерева страницы.
Base64 — встраивание бинарных данных прямо в HTML/CSS
Base64 — это кодировка, которая преобразует бинарные данные (изображение) в ASCII-строку. Полученную строку можно встроить прямо в HTML или CSS без отдельного HTTP-запроса:
<!-- В HTML -->
<img src="data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA" alt="Иконка">
<!-- В CSS -->
.icon {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==');
}
Ключевые особенности Base64:
- Увеличение размера на ~33% — Base64-строка примерно в 1.37 раза больше исходного бинарного файла. Изображение в 100 КБ превращается в ~137 КБ текста.
- Нет отдельного HTTP-запроса — это плюс для очень маленьких иконок (до 1-2 КБ), так как экономит время на TCP-handshake и TLS- negotiation. Но для больших изображений это минус: строка увеличивает размер HTML-документа, который блокирует рендеринг.
- Нет отдельного кэширования — если одно и то же изображение используется на 10 страницах, обычный
<img src>загрузит его один раз и закэширует. Base64-версия будет дублироваться в HTML каждой страницы. - Применим к любому формату — можно кодировать PNG, JPEG, WebP, SVG (хотя для SVG лучше использовать URL-encoding, а не Base64).
Сравнительная таблица:
| Критерий | <img> с файлом | Inline SVG | Base64 |
|---|---|---|---|
| HTTP-запрос | Да | Нет | Нет |
| Масштабирование | Пикселизация | Без потерь | Зависит от формата |
| Кэширование браузером | Да (отдельно) | Как часть HTML | Как часть HTML |
| CSS-стилизация | Ограниченная | Полная (fill, stroke, transform) | Ограниченная |
| Размер HTML-документа | Минимальный | Увеличивается | Увеличивается на ~33% |
| Оптимально для | Фото, сложные изображения | Иконки, логотипы, диаграммы | Микро-иконки до 1-2 КБ |
Практические рекомендации:
- Фотографии и сложные изображения — всегда
<img>с современными форматами (WebP, AVIF) иsrcsetдля responsive. - Иконки и простая графика — inline SVG для полного контроля через CSS и JS, или SVG-спрайты.
- Base64 — только для очень маленьких изображений (до 1-2 КБ), которые критичны для первого рендера (например, favicon или мелкая иконка в критическом CSS). Для всего остального Base64 создаёт больше проблем, чем решает.
Вопрос 4. Как преобразовать изображение, полученное с сервера, для отображения через тег img?
Таймкод: 00:05:14
Ответ собеседника: неполный. В Axios есть преобразователи ответа (responseType), например blob, а также можно использовать встроенные в JS ридеры для преобразования форматов.
Правильный ответ:
Когда сервер возвращает изображение в бинарном виде (например, как массив байтов, Blob или ArrayBuffer), его нужно преобразовать в формат, который понимает атрибут src тега <img>. Есть несколько подходов.
1. Преобразование Blob в Object URL (самый распространённый способ)
Если сервер возвращает изображение как Blob (например, через fetch с responseType: 'blob'), можно создать временный URL через URL.createObjectURL():
async function loadImage() {
const response = await fetch('/api/image/123');
const blob = await response.blob(); // Получаем Blob из ответа
const imageUrl = URL.createObjectURL(blob); // Создаём временный URL
const img = document.getElementById('myImage');
img.src = imageUrl;
// Важно: освободить память, когда изображение больше не нужно
img.onload = () => {
URL.revokeObjectURL(imageUrl);
};
}
URL.createObjectURL() создаёт строку вида blob:http://localhost:3000/550e8400-e29b-41d4-a716-446655440000, которая ссылается на Blob в памяти браузера. URL.revokeObjectURL() освобождает память, когда URL больше не нужен.
2. Преобразование в Base64 через FileReader
Если нужна именно data URI (например, для встраивания в HTML или отправки на сервер), используется FileReader:
async function loadImageAsBase64() {
const response = await fetch('/api/image/123');
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result); // data:image/png;base64,iVBOR...
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Использование
const base64 = await loadImageAsBase64();
document.getElementById('myImage').src = base64;
3. Преобразование ArrayBuffer в Blob, затем в Object URL
Если API возвращает сырые байты (ArrayBuffer), сначала нужно обернуть их в Blob с правильным MIME-типом:
async function loadFromArrayBuffer() {
const response = await fetch('/api/image/123', {
headers: { 'Accept': 'application/octet-stream' }
});
const arrayBuffer = await response.arrayBuffer();
// Оборачиваем в Blob с указанием MIME-типа
const blob = new Blob([arrayBuffer], { type: 'image/webp' });
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
}
4. Использование Axios с responseType: 'blob'
В Axios для получения бинарных данных указывается responseType:
async function loadWithAxios() {
const response = await axios.get('/api/image/123', {
responseType: 'blob' // Важно: по умолчанию 'json', что сломает бинарные данные
});
// response.data уже является Blob
const imageUrl = URL.createObjectURL(response.data);
document.getElementById('myImage').src = imageUrl;
}
5. Если сервер уже возвращает Base64-строку
Иногда API возвращает JSON с полем, содержащим Base64-строку. В этом случае достаточно добавить префикс data URI:
async function loadFromBase64Json() {
const response = await fetch('/api/image/123');
const data = await response.json(); // { image: "iVBORw0KGgo..." }
// Сервер должен указать формат, или мы знаем его заранее
const img = document.getElementById('myImage');
img.src = `data:image/png;base64,${data.image}`;
}
6. Определение MIME-типа
MIME-тип можно определить несколькими способами:
// Из заголовка Content-Type ответа
const contentType = response.headers.get('Content-Type'); // 'image/webp'
// Из первых байтов файла (magic bytes)
function detectImageType(buffer) {
const arr = new Uint8Array(buffer);
if (arr[0] === 0xFF && arr[1] === 0xD8) return 'image/jpeg';
if (arr[0] === 0x89 && arr[1] === 0x50) return 'image/png';
if (arr[0] === 0x52 && arr[1] === 0x49) return 'image/webp'; // RIFF
if (arr[0] === 0x47 && arr[1] === 0x49) return 'image/gif';
return 'application/octet-stream';
}
Сравнение подходов:
| Подход | Плюсы | Минусы |
|---|---|---|
URL.createObjectURL(Blob) | Быстро, не увеличивает размер на 33%, работает с большими файлами | Требует ручного вызова revokeObjectURL |
FileReader.readAsDataURL | Получаем готовую data URI, можно сохранить/передать | Увеличивает размер на ~33%, синхронная блокирующая операция для больших файлов |
| Base64 из API | Просто подставить в src | Увеличивает размер ответа сервера на ~33%, нагрузка на сервер при кодировании |
Практическая рекомендация: для отображения изображений в <img> используйте URL.createObjectURL() — это самый производительный способ. Base64 применяйте только когда нужна именно data URI (например, для встраивания в PDF или отправки через WebSocket).
Вопрос 5. С какими стейт-менеджерами вы работали и какой бы выбрали для нового проекта?
Таймкод: 00:05:50
Ответ собеседника: правильный. Работал с MobX, Effector и Zustand. Для нового проекта выбор зависит от контекста: для небольших проектов — Zustand (легковесный), для крупных — Redux Toolkit (популярный, много специалистов знакомы с ним, удобное разделение бизнес-логики от UI, есть оптимизации вроде мемоизированных селекторов reselect, нормализация данных, альтернатива React Query). В целом выбор зависит от команды и проекта.
Правильный ответ:
Выбор стейт-менеджера — это всегда компромисс между сложностью проекта, опытом команды, требованиями к производительности и экосистеме инструментов. Рассмотрим основные варианты и критерии выбора.
Redux Toolkit (RTK)
Redux Toolkit — это официальная рекомендуемая библиотека для работы с Redux, которая устраняет основные боли «классического» Redux (бойлерплейт, ручная настройка middleware, иммутабельность).
Ключевые преимущества:
createSlice— автоматическая генерация action creators и reducer'ов с поддержкой иммутабельности через Immer (можно писать «мутирующий» код внутри reducer'ов).configureStore— автоматическая настройка Redux DevTools, middleware (thunk по умолчанию).createAsyncThunk— стандартизированная работа с асинхронными операциями.createEntityAdapter— встроенная нормализация данных (CRUD-операции с нормализованным хранилищем).RTK Query— мощный инструмент для работы с API (кеширование, инвалидация, polling, optimistic updates).
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
const userSlice = createSlice({
name: 'user',
initialState: { data: null, status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed' },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.status = 'loading'; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.data = action.payload;
});
},
});
const store = configureStore({ reducer: { user: userSlice.reducer } });
Redux Toolkit оптимален для крупных проектов с большим количеством разработчиков: предсказуемая архитектура, отличные DevTools (time-travel debugging), огромное комьюнити, множество middleware.
Zustand
Минималистичный стейт-менеджер (~1 КБ), не требующий провайдеров, context или бойлерплейта. Основан на паттерне flux, но с максимально простым API.
import { create } from 'zustand';
interface BearState {
bears: number;
increase: () => void;
removeAllBears: () => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
// В компоненте — без провайдеров, просто хук
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears}</h1>;
}
Плюсы: нет бойлерплейта, нет провайдеров, работает вне React (можно читать/писать стор из обычного JS-кода), поддержка middleware (persist, devtools, immer), отличная производительность за счёт селекторов.
Оптимален для небольших и средних проектов, где не нужна строгая архитектура Redux.
MobX
Реактивный стейт-менеджер, основанный на паттерне наблюдатель (observer). Использует прокси/декораторы для автоматического отслеживания зависимостей.
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';
class TimerStore {
seconds = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.seconds += 1;
}
}
const store = new TimerStore();
// Компонент автоматически перерисовывается при изменении отслеживаемых полей
const Timer = observer(() => (
<div>
<p>Seconds: {store.seconds}</p>
<button onClick={() => store.increment()}>+1</button>
</div>
));
Плюсы: декларативность, автоматические вычисляемые значения (computed), реакции (reaction, autorun), минимальный бойлерплейт для простых случаев. Минусы: менее предсказуемый (нет single source of truth в строгом смысле), сложнее отлаживать, меньше экосистема по сравнению с Redux.
Effector
Библиотека, основанная на концепции потоков событий (event-sourcing). Стор строится из атомарных юнитов: events, effects, stores.
import { createEvent, createStore, createEffect, sample } from 'effector';
const increment = createEvent();
const reset = createEvent();
const $counter = createStore(0)
.on(increment, (state) => state + 1)
.reset(reset);
// Эффект (асинхронная операция)
const fetchUserFx = createEffect(async (id: string) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
$counter.watch((value) => console.log('Counter:', value));
Плюсы: мощная композиция, встроенная поддержка сайд-эффектов, отличные DevTools, предсказуемость как в Redux, но с более гибким API. Минусы: специфичная ментальная модель, меньше популярность в англоязычном сообществе.
React Query / TanStack Query
Строго говоря, это не стейт-менеджер, а библиотека для управления серверным состоянием (server state). Но на практике она заменяет значительную часть функциональности Redux/RTK Query для работы с API.
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 10 * 60 * 1000, // 10 минут (ранее cacheTime)
},
},
});
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
Критерии выбора для нового проекта:
- Маленький проект / MVP / прототип — Zustand или даже React Context + useReducer. Не стоит тащить Redux «на всякий случай».
- Средний проект с растущей командой — Zustand или MobX. Zustand проще для онбординга, MobX удобнее если команда привыкла к ООП-подходу.
- Крупный проект с множеством разработчиков — Redux Toolkit + RTK Query (или TanStack Query). Предсказуемая архитектура, строгие контракты, отличные DevTools, легко найти специалистов.
- Проект с интенсивной работой с API — TanStack Query в комбинации с любым клиентским стейт-менеджером. React Query решает задачи кеширования, инвалидации, optimistic updates, повторных запросов — то, что в Redux приходится писать вручную.
Важно понимать: в современных проектах часто используется комбинация — например, Zustand для клиентского состояния (UI-флаги, фильтры, тема) + TanStack Query для серверного состояния (данные с API). Это позволяет не хранить в стейт-менеджере то, что и так отлично кешируется через React Query.
Вопрос 6. Что такое CSS-препроцессоры и постпроцессоры? Для чего они используются?
Таймкод: 00:06:03
Ответ собеседника: правильный. Препроцессоры (например, SCSS) расширяют функционал базового CSS: позволяют создавать вложенности, использовать миксины, наследование, импорты, что помогает избежать дублирования кода. Постпроцессоры автоматически добавляют вендорные префиксы в стили.
Правильный ответ:
CSS-препроцессоры
CSS-препроцессоры — это инструменты, которые расширяют возможности стандартного CSS синтаксисом более высокого уровня, а затем компилируют код в обычный CSS, понятный браузеру. Они решают проблемы, которых стандартный CSS на момент их появления не решал: дублирование, отсутствие переменных, невозможность переиспользования блоков стилей.
Основные возможности препроцессоров:
Переменные — хранение повторяющихся значений в одном месте:
$primary-color: #007bff;
$font-stack: 'Inter', sans-serif;
$spacing-unit: 8px;
.button {
background: $primary-color;
font-family: $font-stack;
padding: $spacing-unit * 2;
}
Вложенность (nesting) — структурирование стилей в соответствии с DOM-иерархией:
.card {
border: 1px solid #ddd;
border-radius: 8px;
&__header {
padding: 16px;
font-weight: bold;
}
&__body {
padding: 16px;
}
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
Миксины (mixins) — переиспользуемые блоки стилей с параметрами:
@mixin flex-center($direction: row) {
display: flex;
flex-direction: $direction;
justify-content: center;
align-items: center;
}
@mixin responsive($breakpoint) {
@if $breakpoint == mobile {
@media (max-width: 768px) { @content; }
} @else if $breakpoint == tablet {
@media (max-width: 1024px) { @content; }
}
}
.modal {
@include flex-center(column);
@include responsive(mobile) {
width: 100%;
padding: 8px;
}
}
Наследование (extend) — переиспользование набора свойств:
%button-base {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
border: none;
}
.button-primary {
@extend %button-base;
background: #007bff;
color: white;
}
.button-danger {
@extend %button-base;
background: #dc3545;
color: white;
}
Импорты и партиалы — разделение стилей на модули:
// _variables.scss
$primary: #007bff;
// _mixins.scss
@mixin flex-center { ... }
// main.scss
@use 'variables';
@use 'mixins';
// Современный синтаксис SCSS (@use вместо @import)
// @use создаёт пространство имён, предотвращает конфликты
Операции и функции — математические вычисления и встроенные функции:
.container {
width: 100% - 32px;
margin: 16px auto;
}
$base-font: 16px;
body {
font-size: $base-font;
line-height: $base-font * 1.5;
color: darken($primary-color, 15%);
}
Популярные препроцессоры:
- SCSS/Sass — самый популярный, два синтаксиса (SCSS — похож на CSS, Sass — отступы вместо скобок). Богатая экосистема, множество фреймворков (Compass, Bourbon).
- Less — использовался в Bootstrap до версии 4, компилируется через JavaScript (less.js).
- Stylus — гибкий синтаксис (можно писать со скобками, без скобок, без двоеточий), использовался в экосистеме Node.js.
CSS-постпроцессоры
Постпроцессоры работают с уже написанным CSS-кодом: берут валидный CSS, анализируют его через AST (Abstract Syntax Tree) и модифицируют — добавляют вендорные префиксы, минифицируют, трансформируют современный CSS для старых браузеров.
Autoprefixer — главный и самый важный постпроцессор. На основе данных Can I Use и заданной конфигурации браузеров (browserslist) автоматически добавляет вендорные префиксы:
/* Вход */
.example {
display: flex;
user-select: none;
backdrop-filter: blur(10px);
}
/* Выход (для целевых браузеров) */
.example {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
Конфигурация через browserslist в package.json:
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
PostCSS — это не конкретный инструмент, а платформа для создания CSS-плагинов. Autoprefixer — это плагин для PostCSS. Другие полезные плагины:
- postcss-preset-env — позволяет использовать современный CSS (custom properties, nesting,
@mediarange queries) и компилирует его для старых браузеров. По сути, это Babel для CSS. - cssnano — минификация CSS (удаление пробелов, оптимизация значений, объединение дублирующихся правил).
- postcss-merge-rules — объединение одинаковых наборов свойств из разных селекторов.
- postcss-custom-properties — компиляция CSS-переменных (custom properties) в статические значения для браузеров, не поддерживающих
var(). - Stylelint — линтер CSS (аналог ESLint), тоже работает через PostCSS.
Типичная конфигурация PostCSS:
// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 2,
features: {
'nesting-rules': true,
'custom-properties': { preserve: true },
},
}),
require('autoprefixer'),
...(process.env.NODE_ENV === 'production' ? [require('cssnano')] : []),
],
};
Современный контекст:
С развитием нативного CSS многие функции препроцессоров стали доступны в стандарте: CSS Custom Properties (--var), нативный nesting (&), @layer, color-mix(), clamp(), контейнерные запросы (@container). Это снижает потребность в препроцессорах, но они по-прежнему полезны для сложных миксинов, циклов, условий и продвинутой организации кода.
Постпроцессоры, напротив, остаются актуальными: Autoprefixer по-прежнему необходим, а PostCSS-экосистема расширяется, позволяя использовать через плагины экспериментальные CSS-фичи до их нативной поддержки в браузерах.
Вопрос 7. В чём разница между var, let и const?
Таймкод: 00:07:17
Ответ собеседника: правильный. var имеет функциональную область видимости и всплывает (hoisting), при обращении до объявления возвращает undefined. let и const имеют блочную область видимости, при обращении до объявления вызывают ReferenceError. let позволяет переприсваивать значение, const — нет, но можно изменять содержимое ссылочных типов данных, объявленных через const.
Правильный ответ:
Область видимости (Scope)
var имеет функциональную область видимости: переменная видна во всей функции, в которой объявлена, независимо от блока, в котором находится объявление. let и const имеют блочную область видимости: переменная существует только внутри ближайших фигурных скобок {}.
function example() {
if (true) {
var x = 1;
let y = 2;
const z = 3;
}
console.log(x); // 1 — var видна за пределами блока if
console.log(y); // ReferenceError — let ограничена блоком if
console.log(z); // ReferenceError — const ограничена блоком if
}
Hoisting (всплытие объявлений)
var подвергается hoisting: объявление переменной перемещается на верх функции, но инициализация остаётся на месте. До присваивания переменная содержит undefined:
console.log(a); // undefined — объявление всплыло, значение ещё не присвоено
var a = 5;
// Интерпретатор видит это как:
// var a;
// console.log(a);
// a = 5;
let и const тоже всплывают, но попадают в «временную мёртвую зону» (Temporal Dead Zone, TDZ). Объявление зарезервировано, но до строки инициализации обращение к переменной вызывает ReferenceError:
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
// TDZ начинается с начала блока и заканчивается строкой объявления
Переопределение и повторное объявление
var позволяет объявлять одну и ту же переменную несколько раз в одной области видимости без ошибки — повторные объявления просто игнорируются:
var x = 1;
var x = 2; // Никакой ошибки, x === 2
let и const запрещают повторное объявление в одной области видимости:
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
Переприсваивание
let позволяет изменять значение переменной. const запрещает переприсваивание после инициализации:
let count = 0;
count = 1; // OK
const MAX_SIZE = 100;
MAX_SIZE = 200; // TypeError: Assignment to constant variable
Важно: const защищает только ссылку, а не содержимое объекта. Объекты и массивы, объявленные через const, можно мутировать:
const user = { name: 'Alice' };
user.name = 'Bob'; // OK — мутация объекта
user = { name: 'Charlie' }; // TypeError — переприсваивание ссылки
const numbers = [1, 2, 3];
numbers.push(4); // OK — [1, 2, 3, 4]
numbers = [5, 6]; // TypeError
Если нужна настоящая неизменяемость, используйте Object.freeze() (поверхностная заморозка):
const config = Object.freeze({ apiUrl: 'https://api.example.com', timeout: 5000 });
config.timeout = 10000; // Тихо игнорируется в strict mode, бросает TypeError в strict mode
Привязка к глобальному объекту
В глобальной области видимости var создаёт свойство на объекте window (в браузере), а let и const — нет:
var globalVar = 'hello';
let globalLet = 'world';
console.log(window.globalVar); // 'hello'
console.log(window.globalLet); // undefined
Практические рекомендации:
- По умолчанию используйте
const— для значений, которые не будут переприсваиваться. - Используйте
letкогда значение будет меняться (счётчики циклов, аккумуляторы). - Не используйте
varв новом коде — это устаревший механизм с непредсказуемым поведением из-за hoisting и функциональной области видимости. Единственное обоснование дляvar— поддержка очень старого кода или специфические edge cases.
Вопрос 8. Что такое IIFE (Immediately Invoked Function Expression) и для чего она используется?
Таймкод: 00:08:06
Ответ собеседника: неполный. IIFE — это функция, которая вызывается сразу после создания. Используется для создания замыканий, чтобы не создавать отдельную переменную и сразу вызвать функцию. Переменная, объявленная через const внутри IIFE, не видна вне функции, так как функция создаёт блочную область видимости.
Правильный ответ:
IIFE (Immediately Invoked Function Expression) — это функциональное выражение, которое выполняется сразу же после определения. Синтаксически функция оборачивается в круглые скобки, чтобы интерпретатор воспринял её как выражение, а не как объявление функции, после чего сразу вызывается парой скобок ():
(function() {
console.log('Выполнено сразу!');
})();
// Стрелочная версия
(() => {
console.log('Выполнено сразу!');
})();
Зачем нужны скобки вокруг функции: если написать function() { }(), парсер воспримет function как начало FunctionDeclaration, а FunctionDeclaration не может быть немедленно вызвана — будет SyntaxError. Скобки превращают объявление в выражение, которое можно вызвать.
Основные сценарии использования:
1. Создание изолированной области видимости (до появления let/const)
До ES6 единственным способом создать локальную область видимости для переменных была функция. IIFE позволяла «спрятать» переменные от глобальной области:
(function() {
var secretKey = 'abc123';
// secretKey не загрязняет глобальную область
initializeApp(secretKey);
})();
console.log(typeof secretKey); // 'undefined'
С появлением let и const с блочной областью видимости эта задача решается проще:
{
const secretKey = 'abc123';
initializeApp(secretKey);
}
// secretKey недоступна здесь
2. Модульный паттерн (Module Pattern)
IIFE использовалась для создания модулей с приватным и публичным API до появления ES-модулей:
const CounterModule = (function() {
// Приватное состояние
let count = 0;
// Приватный метод
function log(action) {
console.log(`Counter ${action}, value: ${count}`);
}
// Публичный API
return {
increment() {
count++;
log('incremented');
},
decrement() {
count--;
log('decremented');
},
getValue() {
return count;
},
};
})();
CounterModule.increment(); // Counter incremented, value: 1
CounterModule.increment(); // Counter incremented, value: 2
console.log(CounterModule.getValue()); // 2
// CounterModule.count — undefined, приватная переменная недоступна
3. Сохранение контекста в циклах (до let)
Классическая проблема var в циклах — все замыкания захватывают одну и ту же переменную:
// Проблема: выведет 5, 5, 5, 5, 5
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
// Решение через IIFE: выведет 0, 1, 2, 3, 4
for (var i = 0; i < 5; i++) {
(function(capturedI) {
setTimeout(() => console.log(capturedI), 100);
})(i);
}
// Современное решение через let: выведет 0, 1, 2, 3, 4
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100);
}
4. Вычисление при инициализации
IIFE позволяет выполнить сложную логику при инициализации модуля:
const config = (function() {
const env = detectEnvironment();
const apiUrl = env === 'production'
? 'https://api.prod.com'
: 'https://api.staging.com';
return Object.freeze({
apiUrl,
timeout: env === 'production' ? 5000 : 30000,
debug: env !== 'production',
});
})();
5. Передача зависимостей
IIFE может принимать параметры, что удобно для инъекции зависимостей и защиты от переопределения глобальных переменных:
(function($, window, document) {
// Здесь $ гарантированно jQuery, даже если кто-то переопределил $
$(document).ready(function() {
// ...
});
})(jQuery, window, document);
IIFE с именованной функцией (Named FE):
(function init() {
console.log('Инициализация...');
// Рекурсивный вызов возможен по имени
// init();
})();
Современная актуальность:
С появлением ES-модулей (import/export), let/const с блочной областью видимости и классов, IIFE утратила большую часть своей практической необходимости. Однако она по-прежнему встречается в:
- Старых библиотеках и кодовых базах (jQuery-плагины, legacy-проекты).
- Скриптах, которые должны работать без сборщика (inline-скрипты в HTML).
- UMD-обёртках для библиотек, которые должны работать и в браузере, и в Node.js.
- Ситуациях, когда нужно выполнить асинхронную инициализацию с
await(top-level await поддерживается не везде):
(async () => {
const data = await fetch('/api/config').then(r => r.json());
initializeApp(data);
})();
Вопрос 9. Что такое контекст (this) в JavaScript?
Таймкод: 00:09:26
Ответ собеседника: правильный. Контекст — это ссылка на объект, который владеет текущим исполняемым кодом. Глобальный контекст — это window (или undefined в strict mode). Внутри function declaration контекст будет другим объектом. Контекст можно переопределять с помощью bind, call, apply.
Правильный ответ:
this в JavaScript — это специальная переменная, которая ссылается на объект, в контексте которого выполняется текущая функция. В отличие от большинства языков, где this жёстко привязан к экземпляру класса, в JavaScript this определяется тем, как функция была вызвана, а не тем, где она была объявлена.
Правила определения this:
1. Глобальный контекст
В глобальной области видимости (вне любой функции) this ссылается на глобальный объект: window в браузере, globalThis в любой среде:
console.log(this === window); // true (в браузере, не strict mode)
console.log(this === globalThis); // true (универсально)
В strict mode ('use strict') глобальный this равен undefined:
'use strict';
console.log(this); // undefined (в глобальной области)
2. Контекст метода объекта
Когда функция вызывается как метод объекта, this указывает на этот объект:
const user = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
},
};
user.greet(); // 'Hello, Alice' — this === user
Важно: если метод извлечь из объекта и вызвать отдельно, контекст теряется:
const greetFn = user.greet;
greetFn(); // 'Hello, undefined' — this === window (или undefined в strict mode)
3. Конструктор (new)
При вызове функции через new, this указывает на новый созданный объект:
function Person(name) {
this.name = name;
}
const person = new Person('Bob');
console.log(person.name); // 'Bob'
4. Явная привязка: call, apply, bind
Эти методы позволяют явно указать, чему равен this:
function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = { name: 'Charlie' };
introduce.call(person, 'Hi', '!'); // "Hi, I'm Charlie!"
introduce.apply(person, ['Hello', '.']); // "Hello, I'm Charlie."
// bind создаёт новую функцию с привязанным контекстом
const boundIntroduce = introduce.bind(person, 'Hey');
boundIntroduce('?'); // "Hey, I'm Charlie?"
Разница: call и apply вызывают функцию сразу (разница только в передаче аргументов — через запятую или массив), bind возвращает новую функцию с привязанным контекстом, не вызывая её.
5. Стрелочные функции (Arrow Functions)
Стрелочные функции не имеют собственного this. Они захватывают this из лексического (объемлющего) контекста — того контекста, в котором были определены:
const team = {
name: 'Frontend',
members: ['Alice', 'Bob'],
// Обычный метод — this === team
showTeam() {
console.log(`Team: ${this.name}`);
// Стрелочная функция внутри метода — this унаследован от showTeam
this.members.forEach((member) => {
console.log(`${member} in ${this.name}`); // this === team
});
// Обычная функция внутри метода — this === window/undefined
this.members.forEach(function(member) {
console.log(`${member} in ${this.name}`); // this !== team!
});
},
};
team.showTeam();
Именно поэтому стрелочные функции нельзя использовать как конструктор и нельзя изменить их this через call/apply/bind — они просто игнорируют явную привязку:
const obj = { value: 42 };
const arrowFn = () => console.log(this.value);
arrowFn.call(obj); // this НЕ станет obj — стрелочная функция игнорирует call
6. Обработчики событий DOM
В обработчиках событий this указывает на элемент, на котором произошло событие:
button.addEventListener('click', function() {
console.log(this); // <button> элемент
});
// Но со стрелочной функцией:
button.addEventListener('click', () => {
console.log(this); // window/undefined (унаследован из внешнего контекста)
});
Приоритет правил (от высшего к низшему):
new— если функция вызвана как конструктор,this— новый объект.bind— если функция создана черезbind,this— привязанный объект (нельзя переопределить даже черезcall/apply).call/apply— явная привязка при вызове.- Вызов как метод объекта (
obj.method()) —this— объект слева от точки. - Свободный вызов (
func()) —this—window/globalThis(илиundefinedв strict mode). - Стрелочная функции —
thisнаследуется из лексического контекста, все правила выше игнорируются.
Практический пример с классами:
class Timer {
constructor() {
this.seconds = 0;
// Проблемы: this теряется при передаче метода как callback
// Решение 1: bind в конструкторе
this.tick = this.tick.bind(this);
// Решение 2: стрелочная функция как свойство (class field)
this.reset = () => {
this.seconds = 0;
};
}
tick() {
this.seconds++;
console.log(this.seconds);
}
}
const timer = new Timer();
setInterval(timer.tick, 1000); // Работает благодаря bind в конструкторе
Современная альтернатива: в большинстве случаев проблемы с this можно избежать, используя стрелочные функции и не полагаясь на динамический контекст. В React, например, class field со стрелочными функциями стали стандартом для обработчиков событий.
Вопрос 10. Чем отличаются методы bind, call и apply? Что произойдёт при вызове трёх bind подряд?
Таймкод: 00:10:07
Ответ собеседника: правильный. bind создаёт новую функцию с привязанным контекстом и не вызывает её сразу, а возвращает обёртку. call и apply вызывают функцию сразу; call принимает аргументы через запятую, apply — в виде массива. При вызове трёх bind подряд сработает только первый, так как у функции, возвращённой первым bind, уже привязан контекст, и последующие bind не перезапишут его.
Правильный ответ:
Все три метода — call, apply, bind — унаследованы от Function.prototype и предназначены для управления значением this при вызове функции. Различаются моментом вызова и способом передачи аргументов.
call — немедленный вызов с перечисленными аргументами
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: 'Alice' };
greet.call(user, 'Hello', '!'); // "Hello, Alice!"
call вызывает функцию сразу, this устанавливается первым аргументом, остальные аргументы передаются через запятую. Подпись: func.call(thisArg, arg1, arg2, ...).
apply — немедленный вызов с аргументами в виде массива (или массивоподобного объекта)
greet.apply(user, ['Hi', '.']); // "Hi, Alice."
apply вызывает функцию сразу, this — первый аргумент, второй аргумент — массив (или массивоподобный объект, например arguments) аргументов функции. Подпись: func.apply(thisArg, argsArray).
Исторически apply был полезен, когда аргументы уже были в виде массива:
const numbers = [5, 3, 8, 1, 9];
// До ES6 — apply был единственным способом передать массив как аргументы
const max = Math.max.apply(null, numbers); // 9
// ES6+ — spread делает то же самое, и apply стал менее необходим
const maxModern = Math.max(...numbers); // 9
bind — создание новой функции с привязанным контекстом (без вызова)
const boundGreet = greet.bind(user, 'Hey');
boundGreet('?'); // "Hey, Alice?"
// Частичное применение (partial application) — аргументы тоже фиксируются
const sayHello = greet.bind(null, 'Hello');
sayHello('!'); // "Hello, undefined!" — this не привязан
bind возвращает новую функцию, не вызывая оригинальную. Подпись: func.bind(thisArg, arg1, arg2, ...). Аргументы, переданные в bind, фиксируются (partial application) и подставляются в начало при последующем вызове.
Наследование bind (chaining)
const obj1 = { name: 'first' };
const obj2 = { name: 'second' };
const obj3 = { name: 'third' };
function show() {
console.log(this.name);
}
const fn = show.bind(obj1).bind(obj2).bind(obj3);
fn(); // "first" — работает ТОЛЬКО первый bind
Почему так: bind создаёт новую обёртку, внутри которой оригинальная функция вызывается с зафиксированным this. Когда мы делаем show.bind(obj1), получаем функцию, которая при любом вызове вызывает show с this === obj1. Когда делаем .bind(obj2) поверх неё, мы пытаемся привязать this к obj2 для обёртки — но обёртки игнорируют внешний this при вызове, потому что внутри неё стоит жёсткий вызов show.call(boundThis, ...). Первый bind «запечатывает» контекст, и последующие bind не могут его перезаписать.
Сравнительная таблица:
| Характеристика | call | apply | bind |
|---|---|---|---|
| Вызов функции | Немедленно | Немедленно | Нет (возвращает новую функцию) |
| Аргументы | Через запятую | Массивом | Через запятую (фиксируются) |
| Возвращает | Результат функции | Результат функции | Новую функцию |
| Частичное применение | Нет | Нет | Да |
| Использование | Нужен результат сразу | Аргументы уже в массиве | Callback'и, обработчики событий |
Практические примеры:
// Заимствование методов — call/apply
function sum() {
// arguments — массивоподобный объект, нет метода reduce
return Array.prototype.reduce.call(arguments, (acc, val) => acc + val, 0);
}
sum(1, 2, 3, 4); // 10
// bind для обработчиков событий
class Component {
constructor() {
this.name = 'MyComponent';
// Без bind — this будет элемент DOM, а не экземпляр класса
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.name);
}
mount(element) {
element.addEventListener('click', this.handleClick);
}
}
Современная альтернатива: spread-оператор (...args) заменил apply в большинстве случаев, а стрелочные функции и class fields снизили потребность в bind для привязки контекста. Тем не менее, bind по-прежнез полезен для частичного применения аргументов и создания специализированных версий функций.
Вопрос 11. Какие методы массивов не мутируют исходный массив?
Таймкод: 00:11:16
Ответ собеседника: неполный. Названы map, filter, slice.
Правильный ответ:
Понимание того, какие методы мутируют массив, а какие — нет, критически важно для предсказуемой работы с данными, особенно в функциональном программировании и в фреймворках вроде React, где иммутабельность — ключевой принцип.
Немутирующие методы (возвращают новый массив или значение, не изменяя оригинал):
map — преобразует каждый элемент, возвращает новый массив той же длины:
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] — оригинал не изменён
filter — отбирает элементы по условию, возвращает новый массив (возможно, короче):
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]
console.log(numbers); // [1, 2, 3, 4, 5]
slice — возвращает копию части массива:
const arr = [1, 2, 3, 4, 5];
const part = arr.slice(1, 3); // [2, 3]
const copy = arr.slice(); // полная копия
console.log(arr); // [1, 2, 3, 4, 5]
concat — объединяет массивы, возвращает новый:
const a = [1, 2];
const b = [3, 4];
const merged = a.concat(b); // [1, 2, 3, 4]
console.log(a); // [1, 2]
flat / flatMap — «выравнивает» вложенные массивы:
const nested = [1, [2, 3], [4, [5]]];
const flat = nested.flat(2); // [1, 2, 3, 4, 5]
const mapped = [1, 2, 3].flatMap(n => [n, n * 2]); // [1, 2, 2, 4, 3, 6]
reduce / reduceRight — сворачивает массив в одно значение (не изменяет массив):
const sum = [1, 2, 3, 4].reduce((acc, n) => acc + n, 0); // 10
find / findIndex / findLast / findLastIndex — поиск элемента, возвращает элемент или индекс:
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const user = users.find(u => u.id === 2); // { id: 2, name: 'Bob' }
const index = users.findIndex(u => u.name === 'Bob'); // 1
indexOf / lastIndexOf / includes — поиск значения, возвращает индекс или boolean:
const arr = [1, 2, 3, 2, 1];
arr.indexOf(2); // 1
arr.lastIndexOf(2); // 3
arr.includes(3); // true
join — объединяет элементы в строку:
const arr = ['a', 'b', 'c'];
const str = arr.join('-'); // "a-b-c"
console.log(arr); // ['a', 'b', 'c']
some / every — проверяют условие, возвращают boolean:
[1, 2, 3].some(n => n > 2); // true
[1, 2, 3].every(n => n > 0); // true
at — доступ по индексу (включая отрицательные):
const arr = [10, 20, 30];
arr.at(-1); // 30
toSorted / toReversed / toSpliced / with (ES2023) — немутирующие версии мутаторов:
const arr = [3, 1, 2];
arr.toSorted(); // [1, 2, 3] — новый отсортированный массив
arr.toReversed(); // [2, 1, 3]
arr.with(0, 99); // [99, 1, 2] — копия с заменой элемента по индексу
console.log(arr); // [3, 1, 2] — оригинал не изменён
toSpliced(start, deleteCount, ...items) — немутирующий аналог splice:
const arr = [1, 2, 3, 4, 5];
const newArr = arr.toSpliced(1, 2, 'a', 'b'); // [1, 'a', 'b', 4, 5]
console.log(arr); // [1, 2, 3, 4, 5]
Spread-оператор и Array.from — также не мутируют:
const original = [3, 1, 2];
const sorted = [...original].sort(); // Сортируем копию
console.log(original); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
Мутирующие методы (изменяют исходный массив):
push/pop— добавление/удаление в конецshift/unshift— удаление/добавление в началоsplice— удаление/вставка элементов по индексуsort— сортировка на местеreverse— разворот на местеfill— заполнение значениемcopyWithin— копирование части массива в другое место того же массива
Важный нюанс: поверхностная копия
Немутирующие методы создают поверхностную (shallow) копию. Вложенные объекты и массивы копируются по ссылке:
const users = [{ name: 'Alice' }, { name: 'Bob' }];
const copy = users.slice();
copy[0].name = 'Charlie';
console.log(users[0].name); // 'Charlie' — объект общий!
copy.push({ name: 'Dave' });
console.log(users.length); // 2 — добавление в новый массив не затрагивает оригинал
Для глубокой копии используйте structuredClone() (нативно в современных браузерах) или JSON.parse(JSON.stringify(arr)) (с ограничениями — не копирует функции, undefined, Date и т.д.).
Вопрос 12. Что такое прототипы в JavaScript и для чего они нужны?
Таймкод: 00:12:13
Ответ собеседника: правильный. Прототипы — это объекты-шаблоны, от которых другие объекты наследуют свойства и методы, чтобы избежать дублирования кода. У функций-конструкторов есть свойство prototype, а у всех объектов есть proto, который является ссылкой на прототип функции-конструктора, с помощью которой был создана объект.
Правильный ответ:
JavaScript — это прототипно-ориентированный язык. В отличие от классического ООП (Java, C#), где наследование строится на основе классов, в JavaScript объекты наследуют свойства и методы непосредственно от других объектов через механизм прототипов.
Ключевые понятия:
1. prototype — свойство функции-конструктора
Каждая функция в JavaScript имеет свойство prototype — это объект, который будет использоваться как прототип для всех экземпляров, созданных через эту функцию с new:
function Animal(name) {
this.name = name;
}
// Добавляем метод в прототип — он будет доступен всем экземплярам
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
const dog = new Animal('Rex');
dog.speak(); // "Rex makes a sound"
2. __proto__ (или [[Prototype]]) — внутренняя ссылка объекта на его прототип
Каждый объект имеет скрытое внутреннее свойство [[Prototype]], которое указывает на объект-прототип. Доступ к нему можно получить через устаревшее свойство __proto__ или через стандартные API:
const dog = new Animal('Rex');
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true (рекомендуемый способ)
3. Цепочка прототипов (Prototype Chain)
Когда вы обращаетесь к свойству объекта, движок сначала ищет его в самом объекте. Если не находит — поднимается по цепочке прототипов до тех пор, пока не найдёт свойство или не достигнет конца цепочки (null):
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Наследование: прототип Dog указывает на экземпляр Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Восстанавливаем ссылку на конструктор
Dog.prototype.bark = function() {
console.log(`${this.name} barks!`);
};
const rex = new Dog('Rex', 'Labrador');
rex.bark(); // "Rex barks!" — найдено в Dog.prototype
rex.speak(); // "Rex makes a sound" — найдено в Animal.prototype (по цепочке)
rex.toString(); // "[object Object]" — найдено в Object.prototype (по цепочке)
// Цепочка: rex → Dog.prototype → Animal.prototype → Object.prototype → null
4. Object.create() — создание объекта с указанным прототипом
const animalProto = {
speak() {
console.log(`${this.name} makes a sound`);
},
};
const cat = Object.create(animalProto);
cat.name = 'Whiskers';
cat.speak(); // "Whiskers makes a sound"
5. Классы ES6 — синтаксический сахар над прототипами
Классы в ES6 не вводят новую модель наследования — это синтаксический сахар над прототипным механизмом:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks!`);
}
}
const rex = new Dog('Rex', 'Labrador');
rex.bark(); // "Rex barks!"
rex.speak(); // "Rex makes a sound"
Под капотом extends устанавливает цепочку прототипов так же, как в примере с Object.create(Animal.prototype).
Зачем нужны прототипы:
- Экономия памяти: методы хранятся в одном месте (на прототипе), а не копируются в каждый экземпляр. Если создан 1000 объектов, метод
speakсуществует в одном экземпляре наAnimal.prototype, а не в тысяче копий. - Динамическое обновление: изменение прототипа влияет на все экземпляры, даже созданные до изменения:
const dog1 = new Dog('Rex', 'Labrador');
const dog2 = new Dog('Max', 'Bulldog');
// Добавляем метод в прототип после создания экземпляров
Dog.prototype.fetch = function() {
console.log(`${this.name} fetches the ball`);
};
dog1.fetch(); // "Rex fetches the ball" — работает!
dog2.fetch(); // "Max fetches the ball" — тоже работает!
- Делегирование: если свойство не найдено в объекте, движок делегирует поиск прототипу — это основа наследования в JavaScript.
Рекомендуемые API для работы с прототипами:
Object.create(proto)— создать объект с указанным прототипом.Object.getPrototypeOf(obj)— получить прототип объекта (вместо__proto__).Object.setPrototypeOf(obj, proto)— установить прототип (не рекомендуется для production из-за производительности).obj.hasOwnProperty(key)— проверить, является ли свойство «собственным» (не унаследованным).obj instanceof Constructor— проверить, входит ли конструктор в цепочку прототипов.
Свойство __proto__ считается deprecated и не рекомендуется к использованию — это нестандартное расширение, которое было добавлено в спецификацию только для совместимости. Всегда используйте Object.getPrototypeOf() и Object.create().
Вопрос 13. Что находится в начале цепочки прототипов?
Таймкод: 00:13:25
Ответ собеседника: неполный. В начале цепочки прототипов находится Object.prototype.
Правильный ответ:
Формулировка «в начале цепочки» может быть неоднозначной, поэтому стоит разобрать оба конца цепочки — начало (откуда наследование стартует) и конец (где оно завершается).
Конец цепочки прототипов — Object.prototype
Object.prototype — это объект, который находится на вершине большинства цепочек прототипов. Его собственный прототип — null, что означает конец цепочки:
const obj = {};
// Цепочка: obj → Object.prototype → null
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Object.prototype содержит базовые методы, доступные всем объектам в JavaScript:
toString()— строковое представление объектаvalueOf()— примитивное значение объектаhasOwnProperty(key)— проверка наличия собственного свойстваisPrototypeOf(obj)— проверка, является ли объект прототипом для другогоpropertyIsEnumerable(key)— проверка перечислимости свойстваtoLocaleString()— локализованное строковое представление
const user = { name: 'Alice' };
user.hasOwnProperty('name'); // true — метод из Object.prototype
user.toString(); // "[object Object]" — метод из Object.prototype
user.toString === Object.prototype.toString; // true
Начало цепочки — конкретный объект
Если под «началом» понимать точку старта поиска свойства, то это сам объект, у которого вы запрашиваете свойство. Поиск всегда начинается с собственных свойств объекта, а затем поднимается по цепочке:
const obj = { ownProp: 'mine' };
obj.ownProp; // Найдено в самом объекте — цепочка не задействована
obj.toString; // Не найдено в obj → найдено в Object.prototype
obj.nonExistent; // Не найдено нигде → undefined
Полная цепочка для типичного объекта:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {};
const dog = new Animal('Rex');
// Цепочка прототипов:
// dog (собственные свойства: name)
// → Animal.prototype (speak)
// → Object.prototype (toString, hasOwnProperty, ...)
// → null
Исключения — объекты без Object.prototype в цепочке:
С помощью Object.create(null) можно создать объект, у которого нет прототипа вообще:
const bareObj = Object.create(null);
console.log(Object.getPrototypeOf(bareObj)); // null
bareObj.toString; // undefined — нет доступа к методам Object.prototype
bareObj.hasOwnProperty; // undefined
// Такие объекты используются как «чистые» словари/мапы,
// где не нужны унаследованные свойства и нет конфликтов имён
Итого: если вопрос подразумевает «что на вершине цепочки» — это Object.prototype. Если «что в начале поиска» — это собственные свойства самого объекта. Цепочка всегда завершается null.
Вопрос 14. Как получить только собственные ключи объекта, а не унаследованные из прототипа?
Таймкод: 00:13:40
Ответ собеседника: неполный. Object.keys возвращает массив ключей, но не уверен, возвращает ли он только собственные. Также можно использовать hasOwnProperty для проверки принадлежности ключа объекту.
Правильный ответ:
В JavaScript есть несколько способов получить только собственные свойства объекта, и важно понимать разницу между ними.
Object.keys() — возвращает массив собственных перечислимых ключей
Да, Object.keys() возвращает только собственные свойства объекта, не затрагивая прототип. При этом возвращаются только перечислимые (enumerable: true) свойства:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {};
const dog = new Animal('Rex');
dog.breed = 'Labrador';
console.log(Object.keys(dog)); // ['name', 'breed'] — только собственные, speak не включён
Object.values() — возвращает массив значений собственных перечислимых свойств
console.log(Object.values(dog)); // ['Rex', 'Labrador']
Object.entries() — возвращает массив пар [ключ, значение]
console.log(Object.entries(dog)); // [['name', 'Rex'], ['breed', 'Labrador']]
Object.getOwnPropertyNames() — все собственные свойства, включая неперечислимые
В отличие от Object.keys(), этот метод возвращает и перечислимые, и неперечислимые свойства (но только строковые, не Symbol):
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 42,
enumerable: false, // неперечислимое свойство
});
obj.visible = true;
console.log(Object.keys(obj)); // ['visible']
console.log(Object.getOwnPropertyNames(obj)); // ['hidden', 'visible']
Object.getOwnPropertySymbols() — собственные свойства с ключами типа Symbol
const id = Symbol('id');
const obj = { [id]: 123, name: 'Alice' };
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id)]
Reflect.ownKeys() — все собственные ключи (строки + Symbols, перечислимые и нет)
Это самый полный метод для получения всех собственных ключей:
const id = Symbol('id');
const obj = {};
obj.name = 'Alice';
Object.defineProperty(obj, 'hidden', { value: 42, enumerable: false });
obj[id] = 123;
console.log(Reflect.ownKeys(obj)); // ['name', 'hidden', Symbol(id)]
hasOwnProperty() — проверка конкретного ключа
dog.hasOwnProperty('name'); // true — собственное свойство
dog.hasOwnProperty('speak'); // false — унаследовано от Animal.prototype
dog.hasOwnProperty('toString'); // false — унаследовано от Object.prototype
Более безопасная версия вызова — через call, чтобы избежать проблем, если метод hasOwnProperty переопределён в объекте:
Object.prototype.hasOwnProperty.call(dog, 'name'); // true
Или используя Object.hasOwn() (ES2022) — современную рекомендованную замену:
Object.hasOwn(dog, 'name'); // true
Object.hasOwn(dog, 'speak'); // false
for...in — перебирает и собственные, и унаследованные перечислимые свойства
В отличие от методов выше, for...in проходит по всей цепочке прототипов:
for (const key in dog) {
console.log(key); // 'name', 'breed', 'speak' — включая унаследованный!
}
// Фильтрация только собственных:
for (const key in dog) {
if (Object.hasOwn(dog, key)) {
console.log(key); // 'name', 'breed'
}
}
Сравнительная таблица:
| Метод | Собственные | Унаследованные | Неперечислимые | Symbols |
|---|---|---|---|---|
Object.keys() | Да | Нет | Нет | Нет |
Object.values() | Да | Нет | Нет | Нет |
Object.entries() | Да | Нет | Нет | Нет |
Object.getOwnPropertyNames() | Да | Нет | Да | Нет |
Object.getOwnPropertySymbols() | Да | Нет | Нет | Да |
Reflect.ownKeys() | Да | Нет | Да | Да |
for...in | Да | Да | Нет | Нет |
Object.hasOwn() / hasOwnProperty() | Проверяет один ключ | — | — | — |
Практическая рекомендация: для получения всех собственных ключей используйте Object.keys() (если не нужны Symbols и неперечислимые свойства) или Reflect.ownKeys() (если нужен полный список). Для проверки конкретного ключа — Object.hasOwn().
Вопрос 15. Почему для перебора обычных объектов используется for...in, а не for...of?
Таймкод: 00:14:59
Ответ собеседния: правильный. for...in используется для перебора объектов, а for...of — для перебора массивов. for...in перебирает также ключи из прототипа, поэтому нужно использовать hasOwnProperty, чтобы отфильтровать только собственные ключи.
Правильный ответ:
Разница между for...in и for...of фундаментальна и связана с тем, какие данные они перебирают и как объекты сообщают о своей итерируемости.
for...in — перечисление ключей (enumeration)
for...in перебирает перечислимые строковые свойства объекта, включая унаследованные по цепочке прототипов. Этот цикл был создан специально для обхода свойств объектов:
const user = { name: 'Alice', age: 30 };
for (const key in user) {
console.log(`${key}: ${user[key]}`);
}
// name: Alice
// age: 30
Особенности:
- Перебирает ключи (имена свойств), а не значения.
- Перебирает как собственные, так и унаследованные перечислимые свойства.
- Порядок итерации не гарантирован для числовых ключей (они идут в порядке возрастания) и не гарантирован для строковых ключей в старых спецификациях (в ES2015+ порядок вставки гарантирован для собственных строковых ключей).
Фильтрация унаследованных свойств:
for (const key in user) {
if (Object.hasOwn(user, key)) {
console.log(`${key}: ${user[key]}`);
}
}
for...of — итерация по значениям (iteration)
for...of работает с объектами, реализующими Symbol.iterator — так называемыми «итерируемыми» (iterable) объектами. Стандартные объекты ({}) не реализуют Symbol.iterator, поэтому for...of для них не работает:
const user = { name: 'Alice', age: 30 };
for (const value of user) {
console.log(value);
}
// TypeError: user is not iterable
for...of работает с теми типами данных, которые имеют встроенный Symbol.iterator:
Array— итерация по элементамString— итерация по символамMap— итерация по парам[key, value]Set— итерация по значениямTypedArray,arguments,NodeListи другие DOM-коллекции
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value); // 10, 20, 30
}
const map = new Map([['name', 'Alice'], ['age', 30]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
Почему обычные объекты не итерируемые:
Исторически обычные объекты (plain objects) не реализуют Symbol.iterator, потому что не было очевидного выбора — итерировать ключи, значения или пары [ключ, значение]? Для этого существуют отдельные методы Object.keys(), Object.values(), Object.entries(), которые возвращают массивы, а массивы уже итерируемы:
const user = { name: 'Alice', age: 30 };
// Итерация по ключам
for (const key of Object.keys(user)) {
console.log(key); // name, age
}
// Итерация по значениям
for (const value of Object.values(user)) {
console.log(value); // Alice, 30
}
// Итерация по парам [ключ, значение]
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
Можно ли сделать объект итерируемым:
Да, реализовав Symbol.iterator:
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const to = this.to;
return {
next() {
if (current <= to) {
return { value: current++, done: false };
}
return { done: true };
},
};
},
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Или через генератор:
const iterableObj = {
*[Symbol.iterator]() {
yield 'a';
yield 'b';
yield 'c';
},
};
for (const val of iterableObj) {
console.log(val); // a, b, c
}
Сравнительная таблица:
| Характеристика | for...in | for...of |
|---|---|---|
| Что перебирает | Ключи (имена свойств) | Значения |
| Прототипные свойства | Да, включает | Нет |
Работает с {} | Да | Нет (TypeError) |
Работает с [] | Да (индексы) | Да (значения) |
Работает с Map/Set | Да | Да |
| Протокол | Enumerable properties | Symbol.iterator |
| Порядок | Порядок вставки + числовые ключи по возрастанию | Порядок, определённый итератором |
Практическая рекомендация: для перебора свойств обычных объектов используйте for...in с проверкой Object.hasOwn(), или предпочтительно — Object.keys()/Object.values()/Object.entries() в сочетании с for...of. Второй подход безопаснее (не подхватывает прототипные свойства) и функциональнее (можно комбинировать с .map(), .filter()).
Вопрос 16. Как можно добавить свой метод в прототип Object?
Таймкод: 00:15:28
Ответ собеседника: правильный. Можно добавить метод в Object.prototype.
Правильный ответ:
Технически добавить метод в Object.prototype очень просто — достаточно присвоить функцию новому свойству:
Object.prototype.myCustomMethod = function() {
return `Hello from ${this.toString()}`;
};
const obj = { name: 'Alice' };
console.log(obj.myCustomMethod()); // "Hello from [object Object]"
// Даже строки, числа и массивы получат этот метод:
console.log('test'.myCustomMethod()); // "Hello from test"
console.log([1, 2].myCustomMethod()); // "Hello from 1,2"
Однако делать это настоятельно не рекомендуется. Добавление свойств в Object.prototype называется «monkey patching» и приводит к серьёзным проблемам.
Проблемы с добавлением в Object.prototype:
1. Загрязнение всех объектов
Поскольку Object.prototype находится в цепочке прототипов практически каждого объекта, новый метод станет виден везде — включая объекты, массивы, функции, экземпляры классов:
Object.prototype.polluted = true;
const emptyObj = {};
console.log(emptyObj.polluted); // true — даже пустой объект имеет это свойство!
2. Ломается for...in без фильтрации
const user = { name: 'Alice' };
for (const key in user) {
console.log(key); // name, polluted — мусорный ключ попал в перебор!
}
3. Конфликты имён
Если два разных фреймворка или библиотеки добавят метод с одинаковым именем в Object.prototype, они будут конфликтовать. Также будущие версии JavaScript могут добавить нативный метод с таким же именем, и ваш метод затрёт его (или наоборот).
4. Сломается проверка hasOwnProperty
const data = JSON.parse('{"name": "Alice"}');
data.hasOwnProperty('name'); // Работает
// Но если кто-то добавил hasOwnProperty в Object.prototype...
Object.prototype.hasOwnProperty = function() { return false; };
data.hasOwnProperty('name'); // false — сломано!
5. Проблемы с библиотеками
Многие библиотеки (jQuery, Lodash, React и другие) полагаются на «чистоту» прототипов. Загрязнение Object.prototype может вызвать непредсказуемые баги.
Безопасные альтернативы:
А. Утилитарные функции (рекомендуется)
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
function deepClone(obj) {
return structuredClone(obj);
}
// Использование
isEmpty({}); // true
Б. Наследование через классы
class EnhancedObject {
constructor(data) {
Object.assign(this, data);
}
toFormattedString() {
return JSON.stringify(this, null, 2);
}
keys() {
return Object.keys(this);
}
}
const user = new EnhancedObject({ name: 'Alice', age: 30 });
console.log(user.toFormattedString());
В. Добавление в конкретный прототип (если необходимо)
Если нужно расширить поведение конкретного типа — добавляйте метод в его прототип, а не в Object.prototype:
// Расширяем Array — затронет только массивы
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3
// Объекты не затронуты
Даже это спорная практика, но она хотя бы не загрязняет глобально все объекты.
Г. Использование Symbol в качестве ключа (если всё же нужно добавить в Object.prototype)
Использование Symbol предотвращает конфликты имён и не видно в for...in:
const myMethod = Symbol('myMethod');
Object.prototype[myMethod] = function() {
return `Custom: ${this}`;
};
const obj = {};
obj[myMethod](); // "Custom: [object Object]"
for (const key in obj) {
console.log(key); // Symbol-свойства не перечисляются
}
Но даже этот подход остаётся monkey patching и несёт риски.
Практическая рекомендация: никогда не добавляйте свойства в Object.prototype в production-коде. Используйте утилитарные функции, классы или модульный подход. Если вы разрабатываете полифилл для стандартного метода — проверяйте его существование перед добавлением:
// Безопасный полифилл
if (!Object.prototype.myStandardMethod) {
Object.prototype.myStandardMethod = function() {
// ...
};
}
Но даже полифиллы лучше подключать через core-js или аналогичные библиотеки, а не вручную.
Вопрос 17. Что такое Virtual DOM, для чего он нужен и как происходит сравнение с реальным DOM-деревом?
Таймкод: 00:16:55
Ответ собеседника: правильный. Virtual DOM — это копия реального DOM-дерева, хранящаяся в оперативной памяти в виде обычного JS-объекта. При изменении состояния сначала меняется Virtual DOM, затем текущая версия сравнивается с предыдущей, вычисляется разница и на основе этого точечно обновляются нужные части реального DOM. Используется эвристический алгоритм на двух предположениях: элементы разных типов производят разные деревья, а ключи помогают React понимать, какие элементы были добавлены, изменены или удалены между рендерами.
Правильный ответ:
Что такое Virtual DOM
Virtual DOM (VDOM) — это легковесное представление реального DOM-дерева в виде обычных JavaScript-объектов, хранящихся в памяти. Каждый узел VDOM содержит информацию о типе элемента, его свойствах и дочерних узлах:
// Реальный DOM: <div className="card"><h2>Hello</h2><p>World</p></div>
// Примерное представление в VDOM:
const vdomNode = {
type: 'div',
props: { className: 'card' },
children: [
{ type: 'h2', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['World'] },
],
};
В React VDOM-узлы создаются через React.createElement() (или JSX, который компилируется в эти вызовы):
// JSX
<div className="card">
<h2>{title}</h2>
<p>{description}</p>
</div>
// Что генерируется:
React.createElement('div', { className: 'card' },
React.createElement('h2', null, title),
React.createElement('p', null, description)
);
Зачем нужен Virtual DOM
Проблема, которую решает VDOM — высокая стоимость операций с реальным DOM. Каждое изменение DOM запускает в браузере цепочку тяжёлых операций: recalculation of styles, layout (reflow), paint, compositing. Прямое манипулирование DOM из JavaScript — это самый медленный способ обновления интерфейса, потому что:
- Каждое обращение к DOM — это переход из JS-движка в рендеринг-движок и обратно (bridge cost).
- Множественные мелкие изменения вызывают множественные reflow и repaint.
- Браузер не может батчить изменения, приходящие из разных мест кода.
VDOM решает эту проблему тремя способами:
Батчинг — все изменения состояния накапливаются, а затем применяются к VDOM за один проход, а не по одному.
Диффинг — вычисляется минимальный набор различий между старым и новым VDOM.
Минимизация операций с реальным DOM — на основе диффа генерируется минимальный патч, который применяется к реальному DOM за один подход.
Алгоритм сравнения (Reconciliation)
React использует эвристический алгоритм сравнения, основанный на двух ключевых предположениях:
Предположение 1: Элементы разных типов порождают разные деревья
Если тип корневого элемента изменился, React не пытается сравнивать их — он полностью размонтирует старое поддерево и создаёт новое:
// Было:
<div><Counter /></div>
// Стало:
<span><Counter /></span>
React размонтирует div (и всё его содержимое, включая Counter с его состоянием) и создаст span с новым Counter с нулевым состоянием. Именно поэтому нельзя менять тип элемента внутри условного рендеринга без ключа:
// Плохой паттерн — тип меняется, состояние теряется
{isText ? <input /> : <textarea />}
Предположение 2: Ключ (key) помогает идентифицировать стабильные элементы
При сравении списков React использует атрибут key для сопоставления элементов между рендерами:
// Без ключей — React сравнивает по позиции:
// Было: [<li>A</li>, <li>B</li>, <li>C</li>]
// Стало: [<li>B</li>, <li>C</li>]
// React: обновить позицию 0 (A→B), обновить позицию 1 (B→C), удалить позицию 2 (C)
// С ключами — React понимает семантику:
// Было: [<li key="a">A</li>, <li key="b">B</li>, <li key="c">C</li>]
// Стало: [<li key="b">B</li>, <li key="c">C</li>]
// React: удалить элемент с key="a", B и C остаются на месте
Использование индекса массива как key — антипаттерн, потому что при вставке/удалении элементов в начале или середине списка индексы сдвигаются, и React неправильно сопоставляет элементы, что приводит к потере состояния и артефактам.
Три уровня сравнения:
1. Сравнение по типу (tree level)
Если типы совпадают — сравниваем дальше. Если нет — полная замена поддерева.
2. Сравнение атрибутов (attribute level)
Если тип элемента тот же, React сравнивает атрибуты и обновляет только изменившиеся:
// Было: <input className="old" value="text" />
// Стало: <input className="new" value="text" />
// Результат: обновить только className в реальном DOM
3. Сравнение дочерних элементов (child level)
Для дочерних элементов React использует алгоритм сравнения списков. Без ключей — по позиции (O(n), но с потерей эффективности при перемещениях). С ключами — по Map (O(n), с корректным отслеживанием перемещений).
Ограничения эвристики:
Алгоритм работает за O(n) благодаря упрощениям, но это цена:
- Элементы не могут перемещаться между разными уровнями вложенности — React не отслеживает такие перемещения.
- Без ключей перемещение внутри списка не оптимизируется.
- Сравнение на уровне поддерева не проводится — если тип изменился, всё поддерево пересоздаётся.
Современные альтернативы:
Стоит отметить, что VDOM — не единственный подход. Svelte компилирует компоненты в императивный код, который напрямую обновляет DOM, без промежуточного слоя. Solid.js использует реактивность на уровне сигналов с гранулярными обновлениями. Однако VDOM остаётся доминирующим подходом в React-экосистеме и доказал свою эффективность на практике.
Вопрос 18. Что произойдёт, если двум элементам задать одинаковые ключи (key) в React?
Таймкод: 00:18:38
Ответ собеседника: правильный. Могут возникнуть проблемы с отображением элементов — неправильно могут отображаться элементы, их состояния и пропсы. Например, в таблице с одинаковыми ключами мог отображаться один и тот же элемент дважды.
Правильный ответ:
Дублирование ключей — одна из самых коварных ошибок в React, потому что она не вызывает падения приложения, но приводит к непредсказуемому поведению, которое сложно отлаживать.
Что делает React с ключами:
React использует ключи для сопоставления элементов между рендерами. Когда алгоритм reconciliation встречает список дочерних элементов, он строит Map ключ → элемент для старого и нового списков, а затем сравнивает их. Ключи должны быть уникальными внутри одного списка (одного уровня вложенности одного родителя) — они не обязаны быть глобально уникальными по всему дереву.
Что происходит при дублировании ключов:
1. React выдаст предупреждение в консоли (development mode)
Warning: Encountered two children with the same key, `foo`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
В production-режиме предупреждение не выводится, но баг остаётся.
2. Второй элемент с дублированным ключом игнорируется
React строит Map ключей. При встрече второго элемента с тем же ключом, он либо перезаписывает первый в Map, либо игнорируется — в зависимости от реализации. Результат: один из элементов не рендерится или рендерится с неправильным содержимым.
// Пример: список с дублированными ключами
const items = [
{ id: 1, name: 'Alice' },
{ id: 1, name: 'Bob' }, // Дубликат!
{ id: 3, name: 'Charlie' },
];
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
// Результат: вместо трёх элементов рендерится два.
// React использует Map: { 1 → <li>Bob</li>, 3 → <li>Charlie</li> }
// Элемент "Alice" потерян.
3. Состояние компонентов смешивается
Если в списке находятся компоненты с внутренним состоянием, дублирование ключей приводит к тому, что React неправильно сопоставляет экземпляры компонентов:
const users = [
{ id: 'a', name: 'Alice' },
{ id: 'a', name: 'Bob' }, // Дубликат!
];
return (
<div>
{users.map(user => (
<UserCard key={user.id} name={user.name} />
))}
</div>
);
function UserCard({ name }) {
const [expanded, setExpanded] = useState(false);
return (
<div onClick={() => setExpanded(!expanded)}>
{name} {expanded ? '▼' : '▶'}
</div>
);
}
При клике на «Alice» React может передать состояние expanded другому элементу, потому что оба имеют один и тот же ключ. Визуально клик по одному элементу может раскрыть другой.
4. Проблемы при сортировке и фильтрации
Дублированные ключи особенно опасны при динамическом изменении порядка элементов:
// До сортировки: key="x" → элемент A, key="x" → элемент B
// После сортировки: key="x" → элемент B, key="x" → элемент A
// React не видит изменений (ключи те же), но содержимое элементов поменялось.
// Результат: артефакты отрисовки, потеря фокуса, сброс анимаций.
Типичные причины дублирования ключей:
// 1. Использование индекса при наличии дубликатов данных
items.map((item, index) => <li key={index}>{item.name}</li>)
// 2. Неуникальные данные из API
// API вернул два объекта с одинаковым id
// 3. Опечатка или копипаста
<li key="user-1">...</li>
<li key="user-1">...</li> // Ошибка: дубликат
// 4. Конкатенация списков без префикса
{[...listA, ...listB].map(item => (
<li key={item.id}>{item.name}</li>
))}
// Если listA и listB содержат элементы с одинаковыми id — дубликаты
Как исправить:
// Используйте стабильные уникальные идентификаторы из данных
items.map(item => <li key={item.id}>{item.name}</li>)
// Для объединённых списков добавляйте префикс
{[...listA.map(item => ({ ...item, key: `a-${item.id}` })),
...listB.map(item => ({ ...item, key: `b-${item.id}` }))].map(item => (
<li key={item.key}>{item.name}</li>
))}
// Если нет естественного ключа — используйте библиотеку для генерации
import { nanoid } from 'nanoid';
const key = nanoid();
Важно помнить: ключи должны быть уникальны только среди соседних элементов (siblings) одного родителя. В разных списках одинаковые ключи допустимы:
<div>
<ul>
{listA.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
<ul>
{listB.map(item => <li key={item.id}>{item.name}</li>)} {/* Допустимо, другой родитель */}
</ul>
</div>
Вопрос 19. Какие методы жизненного цикла классовых компонентов React существуют и какие хуки им соответствуют?
Таймкод: 00:19:29
Ответ собеседника: неполный. componentDidMount соответствует useEffect с пустым массивом зависимостей, componentWillMount — useLayoutEffect. componentWillUpdate соответствует useEffect с массивом зависимостей. При размонтировании используется функция возврата из useEffect с пустым массивом зависимостей. Также упомянут componentDidCatch для отлова ошибок.
Правильный ответ:
Фаза монтирования (Mounting)
constructor(props) — инициализация состояния и привязка методов. Вызывается до первого рендера.
Соответствие в функциональных компонентах: useState для инициализации состояния, useCallback/useMemo для мемоизации:
// Классовый компонент
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, data: null };
}
}
// Функциональный компонент
function MyComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
}
static getDerivedStateFromProps(props, state) — редко используется. Позволяет обновить состояние на основе изменений пропсов. Вызывается перед каждым рендером.
Соответствие: обновление состояния прямо в теле компонента или через useEffect:
// Классовый
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return { prevUserId: props.userId, data: null };
}
return null;
}
// Функциональный
function MyComponent({ userId }) {
const [data, setData] = useState(null);
const [prevUserId, setPrevUserId] = useState(userId);
if (userId !== prevUserId) {
setPrevUserId(userId);
setData(null);
}
}
render() — единственный обязательный метод. Возвращает JSX. Должен быть чистой функцией (без сайд-эффектов).
Соответствие: возвращаемое значение функционального компонента — это и есть render().
componentDidMount() — вызывается после первого рендера, когда компонент уже в DOM. Идеальное место для загрузки данных, подписки на события, инициализации сторонних библиотек.
Соответствие: useEffect с пустым массивом зависимостей:
// Классовый
componentDidMount() {
fetch('/api/data').then(r => r.json()).then(data => this.setState({ data }));
this.subscription = eventBus.subscribe('update', this.handleUpdate);
}
// Функциональный
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(data => setData(data));
const subscription = eventBus.subscribe('update', handleUpdate);
}, []); // Пустой массив = выполнить один раз после монтирования
Фаза обновления (Updating)
Вызывается при изменении пропсов или состояния.
shouldComponentUpdate(nextProps, nextState) — позволяет предотвратить повторный рендер, вернув false. Используется для оптимизации производительности.
Соответствие: React.memo для пропсов, useMemo для вычислений:
// Классовый
shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id;
}
// Функциональный
const MemoComponent = React.memo(function MyComponent({ id, name }) {
return <div>{name}</div>;
}, (prevProps, nextProps) => {
// Кастомная функция сравнения (опционально)
return prevProps.id === nextProps.id; // true = не перерисовывать
});
getSnapshotBeforeUpdate(prevProps, prevState) — вызывается непосредственно перед тем, как изменения будут применены к DOM. Возвращаемое значение передаётся в componentDidUpdate. Используется для сохранения состояния DOM перед обновлением (например, позиции скролла).
Соответствие: нет прямого аналога в хуках. Обычно это решается через useLayoutEffect или через рефы:
// Классовый
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop; // Сохраняем позицию скролла
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
// Функциональный (приближённый аналог)
function MyComponent({ list }) {
const listRef = useRef(null);
const prevLengthRef = useRef(list.length);
useLayoutEffect(() => {
if (prevLengthRef.current < list.length) {
const scrollPos = listRef.current.scrollHeight - listRef.current.scrollTop;
listRef.current.scrollTop = listRef.current.scrollHeight - scrollPos;
}
prevLengthRef.current = list.length;
}, [list.length]);
}
componentDidUpdate(prevProps, prevState, snapshot) — вызывается после обновления DOM. Используется для выполнения сайд-эффектов после изменения (дополнительные запросы, обновление DOM вручную).
Соответствие: useEffect с зависимостями:
// Классовый
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchData(this.props.userId);
}
}
// Функциональный
useEffect(() => {
fetchData(userId);
}, [userId]); // Запускается при изменении userId
Фаза размонтирования (Unmounting)
componentWillUnmount() — вызывается непосредственно перед удалением компонента из DOM. Используется для очистки: отмена подписок, таймеров, запросов.
Соответствие: функция очистки, возвращаемая из useEffect:
// Классовый
componentWillUnmount() {
clearInterval(this.timerId);
this.subscription.unsubscribe();
this.abortController.abort();
}
// Функциональный
useEffect(() => {
const timerId = setInterval(() => {}, 1000);
const subscription = eventBus.subscribe('update', handler);
const abortController = new AbortController();
return () => {
clearInterval(timerId);
subscription.unsubscribe();
abortController.abort();
};
}, []);
Обработка ошибок (Error Handling)
static getDerivedStateFromError(error) — вызывается при ошибке в дочернем компоненте. Позволяет обновить состояние для отображения fallback UI.
componentDidCatch(error, errorInfo) — вызывается при ошибке, используется для логирования.
Соответствие: в хуках нет прямого аналога. Для обработки ошибок используются Error Boundaries — классовые компоненты:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
Для обработки ошибок в асинхронном коде и эффектах используется react-error-boundary библиотека или хук useErrorBoundary (экспериментальный).
Устаревшие методы (deprecated):
componentWillMount→useEffect(() => {}, [])или инициализация в конструкторе/теле компонентаcomponentWillReceiveProps→useEffectс зависимостью от пропсаcomponentWillUpdate→useEffectс зависимостями илиuseLayoutEffect
useLayoutEffect vs useEffect:
useLayoutEffect — это ближайший аналог componentDidMount/componentDidUpdate по времени выполнения. Он запускается синхронно после всех изменений DOM, но до того, как браузер отрисует изменения. Используется, когда нужно прочитать из DOM и синхронно применить изменения (например, измерить размер элемента и обновить позицию). useEffect запускается асинхронно после рендеринга и не блокирует отрисовку.
Вопрос 20. В чём разница между useEffect и useLayoutEffect?
Таймкод: 00:22:22
Ответ собеседника: правильный. useEffect выполняется асинхронно, после того как компоненты уже отрисовались в браузере. useLayoutEffect выполняется синхронно, в момент, когда компоненты уже появились в DOM-дереве, но ещё не были отрисованы в браузере.
Правильный ответ:
Оба хука используются для выполнения сайд-эффектов, но различаются моментом выполнения относительно рендеринга браузером, что имеет практические последствия.
Порядок выполнения в браузере:
1. React рендерит компонент (вызывает функцию, получает JSX)
2. React применяет изменения к DOM (commit phase)
3. useLayoutEffect выполняется СИНХРОННО ← здесь
4. Браузер отрисовывает пиксели на экране (paint)
5. useEffect выполняется АСИНХРОННО ← здесь
useLayoutEffect — синхронный блокирующий эффект
useLayoutEffect запускается после того, как React обновил DOM, но до того, как браузер отрисовал изменения на экране. Это означает:
- Вы можете прочитать актуальные размеры/позиции элементов из DOM.
- Вы можете синхронно внести изменения в DOM, и пользователь не увидит промежуточного состояния (flickering).
- Выполнение блокирует отрисовку — если эффект тяжёлый, пользователь увидит зависание.
function Tooltip({ targetRef, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
// Выполняется ДО отрисовки — читаем размеры target и вычисляем позицию
const rect = targetRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 8,
left: rect.left,
});
}, [targetRef]);
// Пользователь никогда не увидит tooltip в неправильной позиции,
// потому что useLayoutEffect обновил позицию до отрисовки
return <div style={{ position: 'fixed', ...position }}>{children}</div>;
}
useEffect — асинхронный неблокирующий эффект
useEffect запускается после того, как браузер уже отрисовал изменения. Это означает:
- Эффект не блокирует отрисовку — интерфейс остаётся отзывчивым.
- Возможен визуальный flicker: пользователь может увидеть промежуточное состояние до выполнения эффекта.
- Подходит для большинства сайд-эффектов: загрузка данных, подписки, логирование, отправка аналитики.
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
// Выполняется ПОСЛЕ отрисовки — не блокирует рендеринг
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => setData(data));
}, [userId]);
return <div>{data ? data.name : 'Loading...'}</div>;
}
Когда использовать useLayoutEffect:
- Измерение размеров или позиции элементов (getBoundingClientRect, getComputedStyle).
- Синхронное изменение DOM на основе вычислений, чтобы избежать визуального скачка.
- Синхронизация с анимациями, где важна точная позиция до отрисовки.
Когда использовать useEffect:
- Загрузка данных с API.
- Подписка на события (WebSocket, eventBus, resize observer).
- Установка и очистка таймеров.
- Отправка аналитики, логирование.
- Любые сайд-эффекты, которые не требуют синхронного доступа к DOM.
Практический пример — проблема с useEffect:
// ПРОБЛЕМА: пользователь видит мерцание
function AnimatedBox() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useEffect(() => {
// Выполняется ПОСЛЕ отрисовки
setHeight(ref.current.getBoundingClientRect().height);
}, []);
// Первый рендер: height=0, браузер рисует коробку с height=0
// Затем useEffect срабатывает, height обновляется
// Браузер перерисовывает с новым height → пользователь видит скачок
return <div ref={ref} style={{ height: height || 'auto' }}>Content</div>;
}
// РЕШЕНИЕ: useLayoutEffect предотвращает мерцание
function AnimatedBoxFixed() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// Выполняется ДО отрисовки
setHeight(ref.current.getBoundingClientRect().height);
}, []);
// Браузер рисует сразу с правильным height → нет скачка
return <div ref={ref} style={{ height: height || 'auto' }}>Content</div>;
}
SSR (Server-Side Rendering):
useLayoutEffect не существует на сервере — на сервере нет DOM. React выдаст предупреждение при использовании useLayoutEffect в SSR. Решение — использовать useEffect по умолчанию, а useLayoutEffect только когда действительно нужен синхронный доступ к DOM:
import { useEffect, useLayoutEffect } from 'react';
// Безопасный хук для SSR
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
function MyComponent() {
useIsomorphicLayoutEffect(() => {
// Работает и на клиенте, и на сервере
}, []);
}
Практическая рекомендация: по умолчанию используйте useEffect. Переключайтесь на useLayoutEffect только если столкнулись с визуальным flicker или вам нужно синхронно прочитать из DOM и обновить состояние до отрисовки. Это правило «default to useEffect» рекомендуется самими разработчиками React.
Вопрос 21. Какие методы оптимизации функциональных компонентов React существуют и в чём разница между useMemo и useCallback?
Таймкод: 00:22:52
Ответ собеседника: неполный. Для оптимизации используются React.memo, useMemo и useCallback. useCallback принимает функцию и массив зависимостей, мемоизирует ссылку на функцию, чтобы при каждом рендере ссылка не изменялась.
Правильный ответ:
React.memo — предотвращение повторного рендеринга компонента
React.memo — это компонент высшего порядка (HOC), который мемоизирует результат рендеринга. Если пропсы не изменились (поверхностное сравнение), React повторно использует результат предыдущего рендера:
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data, onUpdate }) {
// Тяжёлые вычисления или сложный JSX
return <div>{/* ... */}</div>;
});
// Кастомная функция сравнения (опционально)
const MemoComponent = React.memo(
function MyComponent({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Вернуть true, чтобы НЕ перерисовывать
return prevProps.user.id === nextProps.user.id;
}
);
Важно: React.memo сравнивает пропсы поверхностно (Object.is). Если передать новый объект или функцию при каждом рендере, React.memo не поможет:
// Бесполезный memo — новый объект при каждом рендере
<ExpensiveComponent config={{ theme: 'dark' }} />
// Бесполезный memo — новая функция при каждом рендере
<ExpensiveComponent onClick={() => handleClick(id)} />
useMemo — мемоизация вычисленного значения
useMemo мемоизирует результат вычислений. Он пересчитывает значение только при изменении зависимостей:
function ProductList({ products, filter }) {
// Фильтрация и сортировка выполняются только при изменении products или filter
const filteredProducts = useMemo(() => {
console.log('Filtering...');
return products
.filter(p => p.category === filter.category)
.sort((a, b) => a.price - b.price);
}, [products, filter.category]);
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}: ${p.price}</li>)}
</ul>
);
}
Без useMemo фильтрация выполнялась бы при каждом рендере — даже если products и filter не изменились, а родительский компонент просто перерисовался по другой причине.
**useMemoтакже используется для стабилизации ссылок на объекты/массивы**, чтобы они не вызывали лишние перерисовки в дочерних компонентах, обёрнутых вReact.memo:
function Parent() {
const [count, setCount] = useState(0);
// Без useMemo — новый объект при каждом рендере, Child перерисовывается
const config = useMemo(() => ({
theme: 'dark',
locale: 'ru',
}), []); // Зависимости не меняются — ссылка стабильна
return <MemoizedChild config={config} />;
}
useCallback — мемоизация ссылки на функцию
useCallback мемоизирует саму функцию. Он возвращает ту же ссылку на функцию, если зависимости не изменились:
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
// Без useCallback — новая функция при каждом рендере TodoItem
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, [setTodos]);
return (
<ul>
{todos.map(todo => (
<MemoizedTodoItem key={todo.id} todo={todo} onDelete={handleDelete} />
))}
</ul>
);
}
Разница между useMemo и useCallback:
useCallback(fn, deps) — это синтаксический сахар для useMemo(() => fn, deps). Они делают одно и то же — мемоизируют ссылку — но для разных типов данных:
// Эти два выражения эквивалентны:
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
const memoizedFn = useMemo(() => () => doSomething(a, b), [a, b]);
| Характеристика | useMemo | useCallback |
|---|---|---|
| Мемоизирует | Результат вычисления (значение) | Ссылку на функцию |
| Возвращает | Любое значение | Функцию |
| Когда использовать | Тяжёлые вычисления, стабилизация объектов | Callback'и для дочерних компонентов в React.memo |
| Аналогия | Кэш результата | Кэш функции |
Другие методы оптимизации:
1. Ленивый импорт (React.lazy + Suspense)
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>
);
}
2. Виртуализация списков
Для длинных списков — react-window или react-virtuoso. Рендерятся только видимые элементы:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%">
{Row}
</FixedSizeList>
);
}
3. Разделение состояния
Размещайте состояние как можно ближе к тем компонентам, которые его используют. Глобальное состояние, изменяющееся часто, вызывает перерисовку всех подписчиков.
4. Избегайте анонимных функций и объектов в пропсах
// Плохо — новый объект и функция при каждом рендере
<Child style={{ color: 'red' }} onClick={() => handleClick()} />
// Хорошо — стабильные ссылки
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => handleClick(), []);
<Child style={style} onClick={onClick} />
Когда НЕ нужно оптимизировать:
Преждевременная оптимизация — корень зла. React.memo, useMemo, useCallback имеют свою цену: они потребляют память на хранение мемоизированных значений и добавляют накладные расходы на сравнение зависимостей. Используйте их только когда:
- Есть измеренная проблема производительности (через React DevTools Profiler).
- Компонент рендерится часто с теми же пропсами.
- Передаёте callback'и в мемоизированные дочерние компоненты.
- Выполняете действительно тяжёлые вычисления (сортировка больших массивов, сложные вычисления).
Вопрос 22. В чём разница между useMemo и useCallback и когда их следует использовать?
Таймкод: 00:23:22
Ответ собеседника: правильный. useMemo используется для мемоизации возвращаемого значения (например, результатов вычислений, сортировок), чтобы они не пересчитывались при каждом рендере. useCallback мемоизирует ссылку на функцию, чтобы при каждом рендере ссылка не изменялась. useCallback стоит использовать в двух случаях: когда функция передаётся через пропсы компоненту, обёрнутому в React.memo, или когда функция передаётся в массив зависимостей других хуков.
Правильный ответ:
Ответ собеседника полный и точный. Рассмотрим тему более развёрнуто для закрепления.
Фундаментальная связь:
useCallback(fn, deps) — это буквально useMemo(() => fn, deps). Оба хука используют один и тот же механизм: они хранят значение между рендерами и возвращают его повторно, пока зависимости не изменились. Разница — в том, что именно они мемоизируют.
useMemo — мемоизация значений
function SearchResults({ query, items }) {
// Тяжёлая фильтрация выполняется только при изменении query или items
const results = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [query, items]);
return <ResultList results={results} />;
}
// Стабилизация объекта конфигурации
function Chart({ data }) {
const config = useMemo(() => ({
type: 'line',
animation: true,
colors: ['#007bff', '#28a745'],
}), []);
return <ChartRenderer data={data} config={config} />;
}
useCallback — мемоизация функций
function TodoList({ todos, onTodoDelete }) {
const [filter, setFilter] = useState('all');
// Функция сохраняет ссылку между рендерами, пока onTodoDelete не изменится
const handleDelete = useCallback((id) => {
onTodoDelete(id);
}, [onTodoDelete]);
// Без useCallback — новая функция при каждом рендере,
// MemoizedTodoItem будет перерисовываться даже с React.memo
return (
<ul>
{todos.map(todo => (
<MemoizedTodoItem key={todo.id} todo={todo} onDelete={handleDelete} />
))}
</ul>
);
}
Два основных случая использования useCallback:
Передача в мемоизированный дочерний компонент:
const MemoizedChild = React.memo(function Child({ onAction }) {
console.log('Child rendered');
return <button onClick={onAction}>Action</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Без useCallback: при каждом рендере Parent создаётся новая функция,
// MemoizedChild получает новый пропс → перерисовывается
// С useCallback: ссылка стабильна → MemoizedChild не перерисовывается
const handleAction = useCallback(() => {
console.log('Action!');
}, []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onAction={handleAction} />
</div>
);
}
Передача в зависимости других хуков:
function DataFetcher({ userId, onDataLoaded }) {
const [data, setData] = useState(null);
// Если onDataLoaded будет новой функцией при каждом рендере,
// useEffect будет перезапускаться бесконечно или слишком часто
const stableCallback = useCallback((newData) => {
onDataLoaded(newData);
}, [onDataLoaded]);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
setData(data);
stableCallback(data);
});
}, [userId, stableCallback]); // stableCallback стабилен благодаря useCallback
return <div>{data?.name}</div>;
}
Антипаттерны — когда НЕ нужна мемоизация:
// 1. Простые вычисления — дешевле пересчитать, чем хранить в памяти
const total = useMemo(() => price * quantity, [price, quantity]);
// Лучше просто:
const total = price * quantity;
// 2. Компонент без React.memo — useCallback бессмысленен
function Parent() {
const handleClick = useCallback(() => {}, []);
// Child НЕ обёрнут в React.memo → useCallback не даёт выигрыша
return <Child onClick={handleClick} />;
}
// 3. Чрезмерная мемоизация на каждом уровне — увеличивает сложность кода
// без реального выигрыша в производительности
Правило: не добавляйте useMemo/useCallback проактивно «на всякий случай». Сначала измерьте производительность через React DevTools Profiler, найдите реальные узкие места, и только потом применяйте оптимизацию там, где она даёт измеримый эффект.
Вопрос 23. Какие ещё способы оптимизации React-приложения существуют, помимо React.memo, useMemo и useCallback?
Таймкод: 00:24:34
Ответ собеседника: неполный. Можно использовать useRef и уменьшать вложенность компонентов — чем меньше вложенность, тем более оптимизирован компонент.
Правильный ответ:
Оптимизация React-приложения — это широкая тема, выходящая далеко за рамки мемозации. Рассмотрим основные направления.
1. Ленивая загрузка кода (Code Splitting)
Разделение бандла на части, которые загружаются по требованию:
// React.lazy для компонентов
const HeavyDashboard = React.lazy(() => import('./Dashboard'));
const HeavySettings = React.lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<HeavyDashboard />} />
<Route path="/settings" element={<HeavySettings />} />
</Routes>
</Suspense>
);
}
// Динамический импорт для утилит
async function handleExport() {
const { generatePDF } = await import('./pdf-generator');
generatePDF(data);
}
На уровне сборщика (Webpack, Vite) также можно настроить разделение вендоров:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['@mui/material'],
},
},
},
},
};
2. Виртуализация списков
Для списков из сотен и тысяч элементов — рендер только видимых элементов:
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function VirtualizedTable({ rows }) {
const Row = ({ index, style }) => (
<div style={style} className="table-row">
<span>{rows[index].name}</span>
<span>{rows[index].value}</span>
</div>
);
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
width={width}
itemCount={rows.length}
itemSize={40}
>
{Row}
</List>
)}
</AutoSizer>
);
}
Библиотеки: react-window (легковесная), react-virtuoso (с динамическими размерами), tanstack-virtual.
3. Оптимизация ре-рендеров через разделение состояния
Размещайте состояние как можно ближе к потребителям, не поднимая его без необходимости:
// Плохо: состояние поднято высоко, перерисовывается всё
function Parent() {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<ExpensiveChildList /> {/* Перерисовывается при каждом вводе символа! */}
</div>
);
}
// Хорошо: состояние инкапсулировано
function Parent() {
return (
<div>
<SearchInput /> {/* Состояние внутри, не влияет на siblings */}
<ExpensiveChildList />
</div>
);
}
function SearchInput() {
const [inputValue, setInputValue] = useState('');
return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
}
4. useReducer вместо множества useState
Когда состояние сложное и обновления зависят друг от друга, useReducer позволяет избежать каскадных ре-рендеров:
// Плохо: три setState подряд — три ре-рендера (без батчинга в React 17)
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
const handleSubmit = () => {
setErrors(validate(name, email)); // Рендер 1
setName(''); // Рендер 2
setEmail(''); // Рендер 3
};
}
// Хорошо: одно обновление состояния
const initialState = { name: '', email: '', errors: {} };
function formReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'SUBMIT':
return { ...initialState, errors: validate(state.name, state.email) };
default:
return state;
}
}
function Form() {
const [state, dispatch] = useReducer(formReducer, initialState);
// Один dispatch = один ре-рендер
}
5. useRef для хранения мутабельных значений без ре-рендеров
useRef хранит значение, которое сохраняется между рендерами, но его изменение не вызывает перерисовку:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
// Сохранение предыдущего значения без ре-рендера
const prevCountRef = useRef(count);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return <div>Now: {count}, Before: {prevCount}</div>;
}
6. Дебаунс и троттлинг для обработчиков событий
import { useMemo, useCallback } from 'react';
function SearchField({ onSearch }) {
// Дебаунс через useMemo + setTimeout
const debouncedSearch = useMemo(() => {
let timeoutId;
return (query) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => onSearch(query), 300);
};
}, [onSearch]);
return <input onChange={e => debouncedSearch(e.target.value)} placeholder="Search..." />;
}
// Или используя хуки-библиотеки
import { useDebouncedCallback } from 'use-debounce';
function SearchField({ onSearch }) {
const debouncedSearch = useDebouncedCallback(onSearch, 300);
return <input onChange={e => debouncedSearch(e.target.value)} />;
}
7. Оптимизация контекста (Context)
React Context вызывает перерисовку всех потребителей при изменении значения. Способы оптимизации:
// Разделение контекста на несколько мелких
// Вместо одного большого контекста:
const AppContext = createContext({ user, theme, notifications, ... });
// Лучше разделить:
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationsContext = createContext(null);
// Потребитель ThemeContext не перерисуется при изменении UserContext
Также можно использовать библиотеки: use-context-selector или jotai для гранулярных подписок.
8. Веб-воркеры для тяжёлых вычислений
Вынос тяжёлых вычислений из основного потока:
// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// Компонент
function DataProcessor({ rawData }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(rawData);
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, [rawData]);
return <div>{result}</div>;
}
9. Оптимизация изображений и ассетов
- Использование современных форматов (WebP, AVIF).
- Ленивая загрузка изображений (
loading="lazy"). - Использование
srcsetдля responsive images. - Сжатие и минификация бандла (tree-shaking, compression).
10. Профилирование и измерение
Без измерений оптимизация слепа:
- React DevTools Profiler — визуализация рендеров, время каждого компонента.
- Chrome Performance tab — анализ long tasks, layout thrashing, paint.
- Lighthouse — общий аудит производительности.
why-did-you-render— библиотека для обнаружения лишних ре-рендеров:
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackAllPureComponents: true,
});
Практический подход: сначала измерить (Profiler), найти узкое место, оптимизировать только его. Не оптимизировать «на всякий случай» — каждый useMemo, useCallback, React.memo добавляет сложность кода и потребляет память на хранение мемоизированных значений.
Вопрос 24. Что такое useRef и для чего он используется?
Таймкод: 00:25:19
Ответ собеседника: правильный. useRef используется для сохранения ссылки на DOM-элементы, а также для хранения значений, изменения которых не должны триггерить рендер. Основная особенность в том, что это один и тот же объект при каждом рендере, и его мутация не вызывает перерендер.
Правильный ответ:
useRef возвращает мутабельный объект { current: initialValue }, который сохраняется на протяжении всего жизненного цикла компонента. Ключевое отличие от useState: изменение ref.current не вызывает повторный рендер.
Основные сценарии использования:
1. Доступ к DOM-элементам
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// После монтирования inputRef.current указывает на <input>
inputRef.current.focus();
}, []);
const handleClick = () => {
inputRef.current.select(); // Выделить текст
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Select All</button>
</div>
);
}
// Измерение размером элемента
function MeasuredBox() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width, height });
}, []);
return (
<div ref={boxRef}>
Size: {dimensions.width} x {dimensions.height}
</div>
);
}
2. Хранение мутабельных значений без ре-рендеров
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null); // Сохраняется между рендерами
const start = () => {
if (intervalRef.current) return; // Уже запущен
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
useEffect(() => {
return () => clearInterval(intervalRef.current); // Очистка при размонтировании
}, []);
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
3. Сохранение предыдущего значения
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current; // Возвращает значение с предыдущего рендера
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Now: {count}, Before: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
4. Хранение экземпляров сторонних библиотек
function MapComponent({ center }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
mapInstanceRef.current = new MapLibre.Map({
container: mapRef.current,
center: [center.lng, center.lat],
zoom: 12,
});
return () => {
mapInstanceRef.current.remove();
};
}, []);
useEffect(() => {
mapInstanceRef.current.setCenter([center.lng, center.lat]);
}, [center]);
return <div ref={mapRef} style={{ width: '100%', height: '400px' }} />;
}
5. Предотвращение замыкания на устаревших значениях (stale closure)
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const latestMessagesRef = useRef(messages);
// Всегда актуальное значение в замыкании
useEffect(() => {
latestMessagesRef.current = messages;
}, [messages]);
useEffect(() => {
const connection = createConnection(roomId);
connection.onMessage = () => {
// Без ref: замыкание захватит устаревшее messages
// С ref: всегда читаем актуальное значение
console.log('Current messages:', latestMessagesRef.current);
};
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
Разница между useRef и useState:
| Характеристика | useRef | useState |
|---|---|---|
| Вызывает ре-рендер при изменении | Нет | Да |
| Значение доступно сразу после изменения | Да | Нет (асинхронно) |
| Использование | DOM-ссылки, мутабельные значения | UI-состояние |
| Сравнение с предыдущим | Всегда тот же объект | Новое значение |
Важные нюансы:
useRefне принимает функцию для ленивой инициализации (в отличие отuseState). Если начальное значение требует вычислений, используйтеuseMemoили вычисляйте при первом рендере.refкак пропс (<div ref={myRef}>) нельзя использовать в функциональных компонентах напрямую — нуженReact.forwardRef.- Изменение
ref.currentв процессе рендера (не в эффекте и не в обработчике) — антипаттерн, ведущий к непредсказуемому поведению.
Вопрос 25. Что такое управляемые и неуправляемые компоненты в React?
Таймкод: 00:25:49
Ответ собеседника: правильный. Управляемые компоненты — это когда каждый обработчик события связан со стейтом, например, у инпута есть value и onChange. Неуправляемые компоненты — когда данные формы хранятся только в DOM-дереве, и получить их можно через useRef.
Правильный ответ:
Управляемые компоненты (Controlled Components)
В управляемых компонентах React-состояние является «единственным источником истины» для значения элемента формы. Каждое изменение ввода обновляет состояние, а состояние обновляет значение ввода — это замкнутый цикл:
function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('user');
const handleSubmit = (e) => {
e.preventDefault();
// Все значения уже в состоянии — просто используем
console.log({ name, email, role });
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<select value={role} onChange={(e) => setRole(e.target.value)}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Submit</button>
</form>
);
}
Преимущества управляемых компонентов:
- Мгновенный доступ к данным — значение всегда в состоянии, не нужно обращаться к DOM.
- Валидация на лету — можно проверять значение при каждом изменении и показывать ошибки.
- Программное управление — можно сбросить форму, установить значения по умолчанию, заблокировать поля — всё через состояние.
- Условный рендеринг — кнопка отправки может быть неактивной, пока форма невалидна.
Недостатки:
- Каждое нажатие клавиши вызывает ре-рендер (для больших форм это может быть заметно).
- Больше бойлерплейта — для каждого поля нужен
useStateиonChange.
Неуправляемые компоненты (Uncontrolled Components)
В неуправляемых компонентах DOM сам управляет данными формы. React не отслеживает изменения — значения читаются из DOM только когда нужно (например, при отправке формы) через useRef:
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// Читаем значения из DOM только в момент отправки
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
file: fileRef.current.files[0],
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="John" placeholder="Name" />
<input ref={emailRef} placeholder="Email" />
<input ref={fileRef} type="file" />
<button type="submit">Submit</button>
</form>
);
}
Обратите внимание: для неуправляемых компонентов используется defaultValue (а не value), потому что value сделал бы компонент управляемым.
Преимущества неуправляемых компонентов:
- Меньше ре-рендеров — React не перерисовывает компонент при каждом вводе.
- Меньше кода — не нужны
useStateиonChangeдля каждого поля. - Интеграция с не-React кодом — нативные формы, плагины, которые работают напрямую с DOM.
- Файловые инпуты —
<input type="file">в любом случае может быть только неуправляемым, потому что его значение доступно только для чтения из соображений безопасности.
Недостатки:
- Нет мгновенного доступа к данным — только через обращение к DOM.
- Валидация на лету сложнее.
- Невозможно программно управлять значениями через состояние.
Сравнительная таблица:
| Критерий | Управляемый | Неуправляемый |
|---|---|---|
| Источник истины | React state | DOM |
| Доступ к данным | Мгновенный (из состояния) | По требованию (из DOM через ref) |
| Ре-рендеры | При каждом изменении | Минимальные |
| Валидация на лету | Простая | Сложная |
| Сброс/установка значений | Через setState | Через DOM API |
| Файловые инпуты | Невозможно | Единственный вариант |
Когда что использовать:
- Управляемые — формы с валидацией на летью, динамические формы (поля добавляются/удаляются), формы с зависимыми полями (выбор страны меняет список городов), формы с отображением промежуточных результатов (live-поиск).
- Неуправляемые — простые формы без валидации, файловые инпуты, интеграция с не-React библиотеками, случаи, где производительность критична (огромные формы с сотнями полей).
Гибридный подход:
Часто используют комбинацию — большинство полей управляемые, а файловые инпуты и редко используемые поля — неуправляемые:
function HybridForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const fileRef = useRef(null); // Файл — неуправляемый
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('name', name);
formData.append('email', email);
formData.append('avatar', fileRef.current.files[0]);
// Отправка...
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<input ref={fileRef} type="file" />
<button type="submit">Submit</button>
</form>
);
}
Альтернатива: библиотеки react-hook-form и formik решают проблему бойлерплейта управляемых компонентов, предоставляя удобный API при сохранении преимуществ управляемого подхода. react-hook-form по умолчанию использует неуправляемые компоненты с регистрацией через ref, что даёт лучшую производительность.
Вопрос 26. Что такое React Profiler и для чего он используется?
Таймкод: 00:26:25
Ответ собеседника: неполный. React Profiler позволяет отслеживать рендеры — смотреть, сколько рендеров происходит и какие элементы перерисовываются. С его помощью можно сначала профилировать приложение, затем применить хуки для оптимизации, снова запустить профайлер и сравнить результаты. Также можно отслеживать значения в контексте и стейтах по структуре дерева React.
Правильный ответ:
React Profiler — это инструмент для измерения и анализа производительности React-приложений. Существует два основных варианта: компонент <Profiler> из React и вкладка Profiler в React DevTools.
React DevTools Profiler
Расширение для браузера (Chrome, Firefox), вкладка «Profiler» позволяет записывать сессии рендеринга и анализировать их:
Основные возможности:
- Flamegraph — визуализация дерева компонентов с цветовой индикацией времени рендеринера. Широкие бары = медленные компоненты. Зелёный = быстрый рендер, жёлтый/красный = медленный.
- Ranked chart — компоненты, отсортированные по времени рендеринга (самые медленные сверху).
- Component renders — количество рендеров каждого компонента за сессию, причины ре-рендеров (state change, props change, parent re-render, hooks changed).
- Timeline — хронология рендеров, позволяет увидеть, какие компоненты рендерились на каждом кадре.
- Interactions — привязка рендеров к конкретным действиям пользователя (клик, ввод) через
traceInteraction.
Как использовать:
- Открыть React DevTools → вкладка Profiler.
- Нажать красную кнопку «Record».
- Выполнить действия в приложении.
- Остановить запись.
- Анализировать flamegraph и искать узкие места.
Компонент <Profiler>
Программный API для измерения производительности конкретных частей дерева:
import { Profiler } from 'react';
function onRenderCallback(
id, // проп "id" компонента <Profiler>
phase, // "mount" | "update" | "nested-update"
actualDuration, // время рендеринга в мс
baseDuration, // время рендеринга без мемоизации (оценка)
startTime, // когда React начал рендер
commitTime, // когда React закоммитил изменения
interactions // Set отслеживаемых взаимодействий
) {
// Отправка метрик в аналитику
analytics.track('component_render', {
component: id,
phase,
duration: actualDuration,
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<Profiler id="MainContent" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
<Footer />
</Profiler>
);
}
Практический цикл оптимизации с Profiler:
- Записать профиль — выполнить целевое действие (открыть страницу, ввести текст, прокрутить список).
- Найти узкое место — в flamegraph найти компоненты с наибольшим
actualDurationили количеством рендеров. - Определить причину — на вкладке компонента посмотреть, почему произошёл ре-рендер (parent re-render, state change, hooks changed).
- Применить оптимизацию —
React.memo,useMemo,useCallback, разделение состояния, виртуализация. - Записать профиль повторно — сравнить
actualDurationи количество рендеров до и после.
Пример анализа:
// Проблема: при вводе текста в SearchInput перерисовывается весь список
function Page() {
const [query, setQuery] = useState('');
const items = useItems(); // 1000 элементов
// Profiler покажет: Item рендерится 1000 раз при каждом нажатии клавиши
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{items.map(item => <Item key={item.id} item={item} />)}
</div>
);
}
// Решение: вынести состояние, мемоизировать
function Page() {
return (
<div>
<SearchInput />
<ItemList />
</div>
);
}
const ItemList = React.memo(function ItemList() {
const items = useItems();
return items.map(item => <Item key={item.id} item={item} />);
});
Важные метрики в Profiler:
- Actual Duration — реальное время рендеринга, включая мемоизацию. Цель: минимизировать.
- Base Duration — время рендеринга без учёта
React.memo/shouldComponentUpdate. Если base ≫ actual — мемоизация работает. - Render count — количество рендеров компонента. Если компонент рендерится чаще, чем ожидалось — причина в лишних обновлениях состояния или пропсов.
- Commit phase — время, которое React тратит на применение изменений к DOM. Должно быть < 16ms для 60fps.
Ограничения:
- Profiler добавляет небольшой overhead, поэтому в production он отключён.
- Для production-мониторинга используют
performance.mark()/performance.measure()или Web Vitals (LCP, FID, CLS). - Profiler не показывает время выполнения эффектов (
useEffect), только рендеринг и commit.
Вопрос 27. Что такое порталы в React и почему нельзя просто использовать высокий z-index для модальных окон?
Таймкод: 00:28:02
Ответ собеседния: правильный. Порталы используются для отрисовки элементов в отдельном DOM-узле, например, для модальных окон, чтобы избежать проблемы с z-index. z-index работает только в рамках одного родительского контекста, и из-за этого компоненты могут перебиваться друг другом. Портал позволяет отрисовать элемент в document.body или другом корневом элементе, минуя родительскую иерархию.
Правильный ответ:
Что такое порталы
Порталы — это механизм React, позволяющий рендерить дочерние элементы в DOM-узел, находящийся вне иерархии DOM-родителя компонента. Реализуется через ReactDOM.createPortal():
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
// Рендерим children в document.body, а не внутри родительского div
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.body // Целевой DOM-узел
);
}
// Использование: компонент вызывается внутри любого места дерева,
// но в DOM он окажется прямо в <body>
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="app">
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal isOpen={isOpen}>
<h2>Modal Title</h2>
<p>Modal content</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</Modal>
</div>
);
}
Почему z-index — это ненадёжное решение
Проблема z-index связана с контекстами наложения (stacking context). В CSS новый контекст наложения создаётся элементами с определёнными свойствами: position с z-index ≠ auto, opacity < 1, transform, filter, will-change, isolation: isolate, mix-blend-mode и другими.
Когда родительский элемент создаёт свой контекст наложения, все дочерние элементы с z-index работают только внутри этого контекста и не могут «выбраться» за его пределы:
<!-- Ситуация: Sidebar с z-index: 1000 перекрывает Modal с z-index: 9999 -->
<div style="position: relative; z-index: 1000;"> <!-- Создаёт stacking context -->
<div class="sidebar" style="position: fixed; z-index: 1;">
Sidebar <!-- z-index: 1 внутри своего контекста = выше всего в нём -->
</div>
</div>
<!-- Modal рендерится в body, но его stacking context — корневой -->
<div class="modal" style="position: fixed; z-index: 9999;">
Modal <!-- z-index: 9999 в КОРНЕВОМ контексте, но sidebar в СВОЁМ контексте с z-index: 1000 -->
</div>
/* Конкретный пример проблемы */
.parent-a {
position: relative;
z-index: 10; /* Создаёт stacking context */
}
.sidebar {
position: fixed;
z-index: 1; /* Внутри parent-a: 10 + 1 = выше, чем всё в корневом контексте */
}
.parent-b {
position: relative;
z-index: 1; /* Другой stacking context */
}
.modal {
position: fixed;
z-index: 9999; /* Внутри parent-b: 1 + 9999, но parent-b.z-index < parent-a.z-index */
}
В этом примере sidebar будет поверх modal, несмотря на z-index: 9999, потому что он находится в stacking context с более высоким z-index родителя. Нет такого значения z-index, которое «пробьёт» границу stacking context родителя.
Проблемы, которые решают порталы:
1. Наложение (stacking context)
Портал рендерит элемент в document.body, минуя все промежуточные stacking context. Модальное окно гарантированно окажется поверх всего.
2. Семантическая корректность и доступность (a11y)
Модальное окно в body легче правильно изолировать для screen readers с помощью aria-modal="true" и управления фокусом (focus trap). Если модал вложен глубоко в DOM, скринридеры могут читать контент под модалом.
3. Управление overflow
Если родительский контейнер имеет overflow: hidden или overflow: auto, модальное окно внутри него будет обрезано. Портал размещает модал вне этих контейнеров.
4. CSS-свойства родителей
filter, transform, will-change на родительских элементах влияют на позиционирование дочерних элементов (например, position: fixed внутри элемента с transform позиционируется относительно этого элемента, а не viewport). Портал решает эту проблему.
Полноценный пример модального окна с порталом:
import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, children }) {
const overlayRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
// Блокировка скролла body
document.body.style.overflow = 'hidden';
// Закрытие по Escape
const handleKeyDown = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleKeyDown);
// Фокус на модал (accessibility)
overlayRef.current?.focus();
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
ref={overlayRef}
className="modal-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}
Важно: порталы сохраняют контекст React. События, происходящие внутри портала, всплывают по React-дереву (не по DOM-дереву). Это означает, что onClick на родительском компоненте поймает клик на портале, хотя в DOM портал находится в document.body. Это поведение часто неожиданно, но именно оно делает порталы удобными — логика компонентов остаётся прежней.
Вопрос 28. Что такое prop drilling и как его избежать?
Таймкод: 00:29:16
Ответ собеседника: правильный. Prop drilling — это ситуация, когда пропсы прокидываются через множество уровней вложенности. Чтобы этого избежать, создаётся контекст (Context) и используется Redux или другие решения.
Правильный ответ:
Что такое prop drilling
Prop drilling — это ситуация, когда данные передаются от родительского компонента к глубоко вложенному дочернему через множество промежуточных компонентов, которые сами эти данные не используют, а только передают дальше:
// Проблема: тема нужна только в DeepChild, но проходит через 3 уровня
function App() {
const [theme, setTheme] = useState('dark');
return <Layout theme={theme} setTheme={setTheme} />;
}
function Layout({ theme, setTheme }) {
// Layout НЕ использует theme, только передаёт дальше
return <Sidebar theme={theme} setTheme={setTheme} />;
}
function Sidebar({ theme, setTheme }) {
// Sidebar тоже НЕ использует theme
return <Panel theme={theme} setTheme={setTheme} />;
}
function Panel({ theme, setTheme }) {
// Только здесь theme реально нужен
return <DeepChild theme={theme} />;
}
Проблемы prop drilling:
- Хрупкость кода — добавление/удаление промежуточного компонента требует изменения всех компонентов в цепочке.
- Загрязнение интерфейсов — компоненты получают пропсы, которые им не нужны, что усложняет их понимание и тестирование.
- Ре-рендеры — при изменении значения перерисовываются все промежуточные компоненты, даже если они мемоизированы (если передаётся новый объект/функция).
Способы избежать prop drilling:
1. React Context
Самое прямое решение для глобальных данных (тема, пользователь, локаль):
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// Промежуточные компоненты НЕ получают theme через пропсы
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <Panel />;
}
function Panel() {
return <DeepChild />;
}
// Только тот компонент, которому реально нужен theme, подписывается
function DeepChild() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>Content</div>;
}
2. Композиция компонентов (Component Composition)
Перестройка архитектуры так, чтобы данные не приходилось прокидывать через промежуточные уровни:
// Вместо prop drilling — передаём компонент как children или как проп
function App() {
return (
<Layout>
<Sidebar>
<Panel>
<DeepChild theme="dark" />
</Panel>
</Sidebar>
</Layout>
);
}
// Или через render props / children
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
return children({ theme, setTheme });
}
function App() {
return (
<ThemeProvider>
{({ theme }) => <DeepChild theme={theme} />}
</ThemeProvider>
);
}
3. Менеджеры состояния (Redux, Zustand, Jotai, Recoil)
Для сложного глобального состояния:
// Zustand
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// Любой компонент может подписаться напрямую
function DeepChild() {
const user = useUserStore((state) => state.user);
return <div>{user?.name}</div>;
}
4. Разделение контекста
Один большой контекст вызывает ре-рендер всех потребителей при любом изменении. Лучше разделить:
// Плохо: один контекст на всё
const AppContext = createContext({ user, theme, notifications, settings });
// Хорошо: отдельные контексты
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationsContext = createContext(null);
// Компонент, подписанный на ThemeContext, не перерисуется при изменении UserContext
5. Библиотеки с гранулярными подписками
jotai и recoil позволяют компонентам подписываться на конкретный атом — ре-рендер только при изменении этого атома:
import { atom, useAtom } from 'jotai';
const themeAtom = atom('dark');
const userAtom = atom(null);
function DeepChild() {
const [theme] = useAtom(themeAtom); // Ре-рендер только при изменении theme
return <div className={theme}>Content</div>;
}
Когда prop drilling — это нормально:
Не нужно сразу тянуть Context или Redux при 2-3 уровнях вложенности. Если цепочка короткая и компоненты логически связаны (например, Form → FormField → Input), передача пропсов — это нормально и даже предпочтительно, потому что делает зависимости явными. Prop drilling становится проблемой при 4+ уровнях вложенности или когда промежуточные компоненты не имеют отношения к передаваемым данным.
Вопрос 29. Что такое Context в React, какие у него плюсы и минусы, и когда лучше использовать Context, а когда — Redux?
Таймкод: 00:29:37
Ответ собеседника: правильный. Context позволяет создать глобальное состояние в приложении и избежать prop drilling. Основной минус — при изменении значения в контексте вызывается рендер всех компонентов, которые его используют. Context лучше подходит для небольших приложений, хранения темы, данных авторизации. Redux лучше для больших приложений, так как он более оптимизирован, имеет инструменты для работы с асинхронными экшенами (middleware), обеспечивает более явное отделение бизнес-логики от UI и не нарушает принцип DRY.
Правильный ответ:
Что такое Context
Context — это встроенный механизм React для передачи данных через дерево компонентов без явной передачи пропсов на каждом уровне. Состоит из трёх частей:
// 1. Создание контекста
const ThemeContext = createContext('light'); // значение по умолчанию
// 2. Провайдер — оборачивает часть дерева и предоставляет значение
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Потребитель — подписывается на значение
function DeepChild() {
const { theme, setTheme } = useContext(ThemeContext);
return <div className={theme}>Content</div>;
}
Плюсы Context:
- Устранение prop drilling — данные доступны любому вложенному компоненту без промежуточной передачи.
- Встроен в React — не требует дополнительных зависимостей.
- Простота — минимальный бойлерплейт для базовых сценариев.
- Типизация — хорошо работает с TypeScript.
Минусы Context:
1. Проблема ре-рендеров
Это главный минус. Когда значение в Provider изменяется, все компоненты, использующие useContext этого контекста, перерисовываются — даже если они используют только часть значения, которая не изменилась:
const AppContext = createContext({ user: null, theme: 'light' });
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// При изменении user перерисовываются ВСЕ потребители,
// включая те, которые используют только theme
return (
<AppContext.Provider value={{ user, theme }}>
<UserPanel /> {/* Использует user */}
<ThemeSwitcher /> {/* Использует theme, но перерисуется при изменении user! */}
</AppContext.Provider>
);
}
Решение — разделение на отдельные контексты:
const UserContext = createContext(null);
const ThemeContext = createContext('light');
2. Нет встроенного middleware
В Redux есть middleware (thunk, saga, RTK Query) для асинхронных операций, логирования, обработки ошибок. В Context нужно реализовывать вручную.
3. Нет DevTools
Redux DevTools предоставляет time-travel debugging, историю действий, состояние в любой момент времени. Для Context таких инструментов нет.
4. Сложность при росте
Когда контекстов становится много, дерево обрастает вложенными провайдерами:
<AuthContext.Provider>
<ThemeContext.Provider>
<LocaleContext.Provider>
<NotificationsContext.Provider>
<UserPreferencesContext.Provider>
<App />
</UserPreferencesContext.Provider>
</NotificationsContext.Provider>
</LocaleContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
Когда использовать Context:
- Статические или редко меняющиеся данные: тема, локаль, настройки UI.
- Данные авторизации: текущий пользователь (изменяется при логине/логауте, то есть редко).
- Небольшие приложения без сложной бизнес-логики.
- Передача зависимостей: конфигурация, фича-флаги, экземпляры сервисов (API-клиенты).
Когда использовать Redux (или аналоги):
- Часто меняющееся состояние: корзина покупок, данные форм, real-time обновления.
- Сложная бизнес-логика: множество взаимосвязанных действий, side effects.
- Потребность в middleware: асинхронные запросы, логирование, аналитика.
- Большая команда: стандартизированная архитектура, DevTools, предсказуемые паттерны.
- Потребность в мемоизированных селекторах: reselect для производных данных.
- Серверное состояние: RTK Query для кеширования, инвалидации, optimistic updates.
Современный подход — комбинация:
В реальных проектах часто используется комбинация: Context для UI-состояния (тема, модальные окна, сайдбар) + Redux/Zustand для бизнес-состояния (данные пользователя, каталог, корзина) + TanStack Query для серверного состояния (загрузка данных с API). Каждый инструмент решает свою задачу, и навязывание одного решения для всего — антипаттерн.
Вопрос 30. Почему в проекте может использоваться MobX вместо Redux?
Таймкод: 00:32:24
Ответ собеседника: правильный. MobX проще в понимании, быстрее осваивается, не нарушает принцип DRY (в отличие от Redux с его boilerplate-кодом). MobX предоставляет больше свободы действий, расширяемости и масштабируемости. Его можно использовать для различных задач: запросы, хранение и получение данных.
Правильный ответ:
Выбор MobX вместо Redux обычно обусловлен несколькими архитектурными и практическими причинами. Рассмотрим ключевые различия.
1. Императивный vs декларативный подход
Redux требует описания каждого изменения через action → reducer → new state. Это много бойлерплейта, но даёт полную предсказуемость. MobX позволяет мутировать состояние напрямую, а реактивная система автоматически уведомляет подписчиков:
// Redux: нужно создать action type, action creator, reducer
const INCREMENT = 'INCREMENT';
const increment = () => ({ type: INCREMENT });
const counterReducer = (state = 0, action) => {
switch (action.type) {
case INCREMENT: return state + 1;
default: return state;
}
};
// MobX: просто меняем значение
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count += 1; // Прямая мутация — MobX отследит автоматически
}
}
2. Автоматическое отслеживание зависимостей
MobX автоматически определяет, какие компоненты зависят от каких данных, и перерисовывает только их. В Redux нужно вручную писать селекторы и использовать React.memo/reselect:
// MobX: компонент автоматически подписывается только на используемые поля
const UserProfile = observer(() => {
const { user } = useStore();
// Перерисовка ТОЛЬКО при изменении user.name, не при изменении user.age
return <div>{user.name}</div>;
});
// Redux: нужен селектор и memo
const userName = useSelector(state => state.user.name);
// Без мемоизации — перерисовка при любом изменении state.user
3. ООП-подход и структурирование кода
MobX хорошо подходит для проектов с богатой предметной областью (Domain-Driven Design). Сторы — это классы с методами, что естественно ложится на ООП-мышление:
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
// Бизнес-логика инкапсулирована в сторе
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
// Вычисляемые значения — автоматически кэшируются
get filteredTodos() {
switch (this.filter) {
case 'active': return this.todos.filter(t => !t.completed);
case 'completed': return this.todos.filter(t => t.completed);
default: return this.todos;
}
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
}
4. Меньше бойлерплейта
Redux Toolkit значительно сократил количество кода по сравнению с классическим Redux, но MobX по-прежнему требует меньше инфраструктурного кода:
// Redux Toolkit: slice + async thunk + extraReducers
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], status: 'idle' },
reducers: {
addTodo: (state, action) => { state.items.push(action.payload); },
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
});
},
});
// MobX: всё в одном месте, мутации напрямую
class TodoStore {
items = [];
status = 'idle';
constructor() {
makeAutoObservable(this);
}
addTodo(text) { this.items.push({ id: Date.now(), text, completed: false }); }
toggleTodo(id) { /* ... */ }
async fetchTodos() {
this.status = 'loading';
this.items = await api.getTodos();
this.status = 'succeeded';
}
}
5. Гранулярные обновления без дополнительных усилий
MobX отслеживает доступ к конкретным свойствам и перерисовывает только те компоненты, которые использовали изменившиеся данные. В Redux для аналогичного поведения нужны reselect, shallowEqual, React.memo.
Когда Redux предпочтительнее MobX:
- Нужна полная предсказуемость и воспроизводимость состояния (time-travel debugging).
- Большая команда, где важна стандартизация и строгие контракты.
- Сложная экосистема middleware (саги, каналы, логирование).
- Потребность в сериализуемом состоянии (SSR, персистентность, отладка).
- Требования к иммутабельности состояния (например, для интеграции с Immutable.js).
Когда MobX предпочтительнее Redux:
- Быстрая разработка, меньше бойлерплейта.
- Богатая предметная область с множеством взаимосвязанных сущностей.
- Команда с ООП-бэкграундом.
- Нужны вычисляемые значения (computed) и реакции (reactions) из коробки.
- Важна производительность без ручной оптимизации селекторов.
Важно: выбор стейт-менеджера — не религиозный вопрос. Для большинства приложений подойдёт любой из них. Критичнее не «какой инструмент», а «как организован код» — разделение ответственности, тестируемость, предсказуемость. В современных проектах также стоит рассмотреть Zustand как компромисс между простотой MobX и предсказуемостью Redux.
Вопрос 31. Какие преимущества TypeScript вы видите и нужен ли он на проектах?
Таймкод: 00:34:06
Ответ собеседника: правильный. TypeScript нужен, потому что он ускоряет разработку — разработчик понимает, с какими типами данных взаимодействует, какие данные приходят с бэкенда, что передаётся в пропсах и аргументах функций. Новым разработчикам легче разобраться в типизированном коде.
Правильный ответ:
TypeScript — это надмножество JavaScript с системой статической типизации. Его преимущества выходят далеко за рамки «просто типизации».
1. Раннее обнаружение ошибок
TypeScript находит ошибки на этапе компиляции, а не во время выполнения. Это критически важно для ошибок, которые в JavaScript проявляются только в рантайме:
// JavaScript: ошибка проявится только при вызове
function getUserName(user) {
return user.name.toUpperCase(); // Если user === null → TypeError в рантайме
}
// TypeScript: ошибка обнаружена до запуска
function getUserName(user: User | null): string {
return user.name.toUpperCase(); // Ошибка: Object is possibly 'null'
}
Типичные ошибки, которые ловит TypeScript:
- Обращение к несуществующему свойству объекта.
- Передача аргумента неверного типа.
- Забытая обработка
null/undefined. - Неправильный возврат из функции.
- Опечатки в именах свойств.
2. Автодополнение и документация в реальном времени
TypeScript даёт IDE информацию о типах, что обеспечивает мощное автодополнение, подсказки и inline-документацию:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
}
function processUser(user: User) {
user. // IDE покажет: id, name, email, role с типами
user.role. // IDE покажет: 'admin' | 'user' | 'moderator'
}
3. Рефакторинг с уверенностью
При переименовании свойства, изменении интерфейса или сигнатуры функции TypeScript покажет все места, которые нужно обновить. В JavaScript приходится полагаться на поиск по тексту, который может пропустить динамические обращения или дать ложные срабатывания.
4. Контракты между модулями и командами
Типы выступают как формальный контракт между частями системы:
// API contract: фронтенд и бэкенд согласовывают интерфейс
interface CreateUserRequest {
name: string;
email: string;
role: 'admin' | 'user';
}
interface CreateUserResponse {
id: number;
createdAt: string;
}
async function createUser(data: CreateUserRequest): Promise<CreateUserResponse> {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data),
}).then(r => r.json());
}
5. Продвинутые возможности типизации
TypeScript предоставляет мощную систему типов, которая позволяет моделировать сложные домены:
// Union types
type Status = 'idle' | 'loading' | 'success' | 'error';
// Discriminated unions
type ApiResponse =
| { status: 'success'; data: User[] }
| { status: 'error'; error: string }
| { status: 'loading' };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'success':
console.log(response.data); // TypeScript знает, что здесь есть data
break;
case 'error':
console.log(response.error); // TypeScript знает, что здесь есть error
break;
}
}
// Generics
function identity<T>(value: T): T {
return value;
}
// Utility types
type PartialUser = Partial<User>; // Все поля опциональные
type UserNames = Pick<User, 'name'>; // Только name
type UserWithoutEmail = Omit<User, 'email'>; // Все кроме email
type ReadonlyUser = Readonly<User>; // Все поля readonly
// Mapped types
type Nullable<T> = { [K in keyof T]: T[K] | null };
6. Самодокументирующийся код
Типы служат живой документацией, которая не устаревает (в отличие от комментариев):
// Без типов: что передавать? Что возвращать? Читать реализацию...
function process(data, options) { ... }
// С типами: всё понятно из сигнатуры
function process(
data: RawUserData,
options: ProcessingOptions
): ProcessedResult { ... }
Когда TypeScript может быть избыточен:
- Прототипы и скрипты — быстрый эксперимент, одноразовый скрипт.
- Очень маленькие проекты — несколько файлов, один разработчик.
- Команда без опыта TS — если команда тратит больше времени на борьбу с типами, чем получает выгоды от них.
- Проекты с очень динамическими данными — когда данные приходят в произвольном формате и типизация требует
anyилиasна каждом шагу.
Практическая рекомендация: для любого проекта, который планируется поддерживать дольше нескольких недель, TypeScript даёт значительную отдачу. Начинать с TS с нуля проще, чем мигрировать с JavaScript потом. Стоимость внедрения — несколько дней на настройку и обучение — окупается за счёт уменьшения багов, ускорения онбординга и безопасного рефакторинга.
Вопрос 32. В чём разница между типами any и unknown в TypeScript?
Таймкод: 00:35:01
Ответ собеседника: правильный. any полностью отключает типизацию — можно вызывать переменную как функцию, обращаться к несуществующим свойствам, и компилятор не подсветит ошибки. unknown — более безопасная версия, с ней нельзя выполнить такие операции без предварительной проверки типа.
Правильный ответ:
any и unknown оба представляют «любое значение», но ведут себя принципиально с точки зрения безопасности типов.
any — «выключатель» типизации
any говорит компилятору: «доверься мне, я знаю, что делаю». С переменной типа any можно делать что угодно — компилятор не будет проверять:
let data: any = fetchData();
data.foo.bar.baz; // ОК — компилятор не проверяет
data(); // ОК — компилятор не проверяет
new data(); // ОК
data.toUpperCase(); // ОК — а вдруг это строка?
// Ошибки проявятся только в рантайме
any «заражает» окружающий код — результат операции с any тоже становится any:
let data: any = fetchData();
let result = data.items; // result тоже any — проверка отключена и для него
unknown — «безопасная» версия any
unknown говорит: «я не знаю, что это за значение, но ты должен доказать его тип перед использованием». С unknown нельзя делать ничего без предварительного сужения типа (type narrowing):
let data: unknown = fetchData();
data.foo; // Ошибка: Object is of type 'unknown'
data(); // Ошибка: Object is of type 'unknown'
data.toUpperCase(); // Ошибка
// Нужно явно проверить тип:
if (typeof data === 'string') {
data.toUpperCase(); // OK — TypeScript знает, что здесь data === string
}
if (data && typeof data === 'object' && 'items' in data) {
(data as { items: unknown[] }).items; // После проверки — OK
}
Сравнительная таблица:
| Операция | any | unknown |
|---|---|---|
| Обращение к свойствам | Разрешено | Требует проверки |
| Вызов как функции | Разрешено | Требует проверки |
| Присвоение в переменную с типом | Разрешено | Требует проверки |
| Результат операций | any (заражает) | unknown (безопасен) |
Когда использовать unknown:
// Обработка данных извне (API, localStorage, JSON)
function parseResponse(jsonString: string): unknown {
return JSON.parse(jsonString);
}
// Типобезопасный парсер
interface User {
id: number;
name: string;
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof data.id === 'number' &&
'name' in data &&
typeof data.name === 'string'
);
}
const raw = parseResponse(responseText);
if (isUser(raw)) {
console.log(raw.name); // OK — TypeScript знает, что это User
}
Когда допустим any:
- Миграция с JavaScript на TypeScript — временное решение для постепенной типизации.
- Работа с динамическими библиотеками, где типы невозможно описать.
- Быстрый прототип, который будет выброшен.
Правило: всегда предпочитайте unknown вместо any. Если вы не знаете тип — используйте unknown и напишите type guard для безопасной работы с данными. any должен быть последним средством, а не стандартным выбором. В конфигурации TypeScript можно включить strict: true и noExplicitAny, чтобы запретить использование any на уровне линтера.
Вопрос 33. В чём разница между типами never и void в TypeScript?
Таймкод: 00:35:44
Ответ собеседника: правильный. never означает, что функция никогда ничего не вернёт — по причине бесконечного цикла или выбросания ошибки. Также never получается при комбинировании двух несовместимых типов. void означает, что функция просто ничего не возвращает (возвращает undefined, если явно не указано иное).
Правильный ответ:
never и void оба описывают «отсутствие значения», но на разных уровнях и с разной семантикой.
void — «функция не возвращает полезного значения»
void означает, что функция завершает выполнение, но не возвращает значение (или возвращает undefined). Функция достигает конца и возвращается в вызывающий код:
function logMessage(msg: string): void {
console.log(msg);
// Нет return — или неявно возвращает undefined
}
function explicitUndefined(): void {
return undefined; // OK — void допускает возврат undefined
}
const result = logMessage('hello');
// result имеет тип void — его нельзя использовать как значение
void — это тип-метка «результат этой функции следует игнорировать». Он полезен для callback'ов, обработчиков событий, побочных эффектов.
never — «функция никогда не завершится»
never означает, что функция никогда не возвращает управление вызывающему коду. Она либо выбрасывает исключение, либо входит в бесконечный цикл:
function throwError(message: string): never {
throw new Error(message);
// После throw выполнение не продолжается — функция не «возвращает» управление
}
function infiniteLoop(): never {
while (true) {
// Функция никогда не завершится
}
}
function fail(): never {
throw new Error('This should never happen');
}
Ключевое отличие:
// void: функция завершается, но не возвращает значение
function doSomething(): void {
console.log('done');
// Выполнение продолжается после вызова
}
// never: функция НЕ завершается (throw или бесконечный цикл)
function doSomethingElse(): never {
throw new Error('fail');
// Выполнение НЕ продолжается после вызова
}
never как «невозможный тип»
never — это тип, который не может содержать ни одного значения. Это делает его полезным в нескольких сценариях:
Исчерпывающая проверка (exhaustive checking):
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// shape имеет тип never — все варианты обработаны
// Если добавить новый вариант в Shape, но не обработать здесь — ошибка компиляции
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}
Если добавить { kind: 'pentagon'; side: number } в Shape, TypeScript покажет ошибку в default ветке, потому что shape уже не будет never — это будет необработанный тип.
never в типах — «невозможное пересечение»:
type A = string & number; // never — значение не может быть одновременно строкой и числом
type Without<T, U> = T extends U ? never : T;
type Result = Without<string | number | boolean, number>; // string | boolean
never в mapped types — удаление свойств:
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined>; // string
Сравнительная таблица:
| Характеристика | void | never |
|---|---|---|
| Семантика | Функция завершается без возврата | Функция никогда не завершится |
| Возвращаемое значение | undefined (неявно) | Нет (throw или ∞ цикл) |
| Присваивание | void можно присвоить в переменную | never можно присвоить в любой тип |
| Тип-теоретически | Единичный тип (одно значение — undefined) | Пустой тип (нуль значений) |
| Использование | Callback'и, побочные эффекты | Исключения, исчерпывающие проверки |
Интересное свойство never:
never является подтипом любого типа, поэтому его можно присвоить в переменную любого типа:
let x: string = fail(); // OK — never является подтипом string
let y: number = fail(); // OK — never является подтипом number
Это логично: функция fail() никогда не вернёт значение, поэтому присвоение никогда не произойдёт, и типобезопасность не нарушается.
Вопрос 34. В чём разница между интерфейсами (interface), типами (type) и абстрактными классами в TypeScript?
Таймкод: 00:36:29
Ответ собеседника: правильный. Интерфейс подходит только для типизации объектов, классов или функций. Тип позволяет типизировать любые данные, включая примитивы и юнионы. Интерфейсы расширяются с помощью extends, типы — с помощью амперсанда (&). Если объявить два интерфейса с одинаковым именем, они объединятся в один; при повторном объявлении type будет ошибка. Абстрактные классы — это классы, экземпляры которых нельзя создавать, от них можно только наследоваться. Наследующий класс должен реализовать все абстрактные методы. В абстрактных классах можно объявлять методы с типизацией, но без логики.
Правильный ответ:
Ответ собеседника полный и точный. Дополним его деталями и примерами.
Interface vs Type — ключевые различия:
Объявление (Declaration Merging)
Интерфейсы поддерживают автоматическое объединение — два интерфейса с одним именем сливаются в один:
interface User {
name: string;
}
interface User {
age: number;
}
// TypeScript объединит их:
const user: User = { name: 'Alice', age: 30 }; // OK
Типы не поддерживают объединение — повторное объявление вызовет ошибку:
type User = { name: string };
type User = { age: number }; // Error: Duplicate identifier 'User'
Это свойство интерфейсов используется для расширения типов из внешних библиотек (declaration merging в .d.ts файлах).
Расширение
// Интерфейсы: extends
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Типы: пересечение (&)
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
Выражения (Computation)
Типы поддерживают вычисляемые выражения, интерфейсы — нет:
// Типы могут быть вычисляемыми:
type Status = 'idle' | 'loading' | 'success' | 'error';
type ApiResponse<T> = { status: 'success'; data: T } | { status: 'error'; error: string };
// Mapped types — только в type:
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Conditional types — только в type:
type IsString<T> = T extends string ? true : false;
// Интерфейсы НЕ могут этого:
// interface Readonly<T> { readonly [K in keyof T]: T[K]; } // Ошибка!
Классы могут реализовать и то, и другое:
interface Printable {
print(): string;
}
type Serializable {
serialize(): string;
}
// Класс может реализовать и interface, и type:
class Document implements Printable, Serializable {
print() { return 'printed'; }
serialize() { return JSON.stringify(this); }
}
Когда что использовать:
- Interface — для описания формы объектов, контрактов классов, публичных API. Если нужна возможность расширения через declaration merging — только interface.
- Type — для union types, mapped types, conditional types, типизиции примитивов, функций, сложных комбинаций.
Абстрактные классы
Абстрактный класс — это рантайм-конструкция (в отличие от interface и type, которые существуют только на этапе компиляции). Он может содержать:
- Абстрактные методы (без реализации, только сигнатура).
- Реализованные методы (с логикой).
- Свойства (поля) с инициализацией.
- Конструктор.
- Модификаторы доступа (
protected,private).
abstract class Shape {
abstract name: string; // Абстрактное свойство
constructor(protected color: string) {}
abstract area(): number; // Абстрактный метод — подкласс ОБЯЗАН реализовать
// Реализованный метод — наследуется как есть
describe(): string {
return `A ${this.color} ${this.name} with area ${this.area()}`;
}
}
class Circle extends Shape {
name = 'circle';
constructor(color: string, private radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
// const shape = new Shape('red'); // Error: Cannot create instance of abstract class
const circle = new Circle('red', 5);
console.log(circle.describe()); // "A red circle with area 78.54..."
Ключевые различия абстрактного класса от интерфейса:
// Интерфейс: только контракт, нет реализации, нет рантайм-кода
interface Logger {
log(message: string): void;
}
// Абстрактный класс: может содержать реализацию и модификаторы доступа
abstract class BaseLogger {
abstract log(message: string): void; // Абстрактный
// Реализованный метод
logError(error: Error): void {
this.log(`ERROR: ${error.message}`);
}
// Protected метод — невозможен в интерфейсе
protected formatMessage(msg: string): string {
return `[${new Date().toISOString()}] ${msg}`;
}
}
Сравнительная таблица:
| Характеристика | interface | type | abstract class |
|---|---|---|---|
| Рантайм-код | Нет | Нет | Да (JavaScript class) |
| Declaration merging | Да | Нет | Нет |
| Union/Intersection types | Нет | Да | Нет |
| Mapped/Conditional types | Нет | Да | Нет |
| Реализация методов | Нет | Нет | Да (частичная) |
| Модификаторы доступа | Нет | Нет | Да |
| implements классом | Да | Да | Нет (используется extends) |
| extends | Да | Через & | Да |
| Конструктор | Нет | Нет | Да |
Вопрос 35. Какие утилитарные типы (utility types) TypeScript вы знаете?
Таймкод: 00:38:17
Ответ собеседника: неполный. Названы Record, Required, Partial, ReturnType.
Правильный ответ:
TypeScript предоставляет множество встроенных утилитарных типов для трансформации существующих типов. Вот полный обзор наиболее полезных.
Partial<T> — все свойства опциональные
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
// Использование: обновление части объекта
function updateUser(id: number, updates: Partial<User>) {
// updates может содержать любое подмножество свойств User
}
Required<T> — все свойства обязательные
interface Config {
apiUrl?: string;
timeout?: number;
}
type RequiredConfig = Required<Config>;
// { apiUrl: string; timeout: number; }
Readonly<T> — все свойства только для чтения
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; }
const user: ReadonlyUser = { id: 1, name: 'Alice' };
user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only property
Pick<T, K> — выбрать указанные свойства
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string; }
// password исключён — не попадает в API-ответ
Omit<T, K> — исключить указанные свойства
type CreateUserRequest = Omit<User, 'id'>;
// { name: string; email: string; password: string; }
// id исключён — он генерируется на сервере
// Альтернатива через Pick + Exclude:
type CreateUserRequest2 = Pick<User, Exclude<keyof User, 'id'>>;
Record<K, V> — объект с ключами K и значениями V
type Role = 'admin' | 'user' | 'moderator';
type RolePermissions = Record<Role, string[]>;
// { admin: string[]; user: string[]; moderator: string[] }
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read'],
moderator: ['read', 'write'],
};
// Практическое использование: маппинг статусов
type HttpStatusMessages = Record<number, string>;
const messages: HttpStatusMessages = {
200: 'OK',
404: 'Not Found',
500: 'Internal Server Error',
};
Exclude<T, U> — исключить из union type указанные значения
type Status = 'idle' | 'loading' | 'success' | 'error';
type NonIdleStatus = Exclude<Status, 'idle'>;
// 'loading' | 'success' | 'error'
Extract<T, U> — оставить только значения, присутствующие в U
type AllTypes = string | number | boolean | null;
type StringOrNumber = Extract<AllTypes, string | number>;
// string | number
NonNullable<T> — исключить null и undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
ReturnType<T> — тип возвращаемого значения функции
function createUser(name: string) {
return { id: Date.now(), name, createdAt: new Date() };
}
type User = ReturnType<typeof createUser>;
// { id: number; name: string; createdAt: Date; }
Parameters<T> — типы аргументов функции (кортеж)
function fetchUser(id: number, includeDeleted: boolean) {
// ...
}
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: number, includeDeleted: boolean]
Awaited<T> — извлечь тип из Promise (ES2022)
type Response = Promise<Promise<string>>;
type Unwrapped = Awaited<Response>; // string
// Полезно для типизации async-функций
async function fetchData(): Promise<{ data: User[] }> {
// ...
}
type Result = Awaited<ReturnType<typeof fetchData>>;
// { data: User[] }
PickByType<T, U> и OmitByType<T, U> — нет встроенных, но часто нужны:
// Выбрать только строковые свойства
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = PickByType<Mixed, string>;
// { name: string; email: string; }
Практические примеры комбинации:
// DTO для обновления пользователя: все поля опциональные, кроме id
type UpdateUserDto = Required<Pick<User, 'id'>> & Partial<Omit<User, 'id'>>;
// Тип для формы: все поля строковые (даже числа — строки из input)
type FormFields<T> = {
[K in keyof T]: string;
};
// DeepPartial — рекурсивный Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// Можно передать { database: { host: 'localhost' } } — всё опционально на любом уровне
Рекомендуемые практики:
- Используйте утилитарные типы вместо дублирования — если изменится базовый тип, все производные обновятся автоматически.
- Не злоупотребляйте сложными цепочками — если тип становится нечитаемым, вынесите промежуточные результаты в отдельные type aliases.
- Для библиотек экспортируйте утилитарные типы вместе с основными — это позволяет потребителям библиотеки легко создавать производные типы.
Вопрос 36. Что такое Type Guard в TypeScript?
Таймкод: 00:38:32
Ответ собеседования: правильный. Type Guard — это функция, которая принимает аргумент и позволяет сузить его тип или определить, к какому типу он относится, с помощью конструкции и оператора instanceof. Внутри функции прописываются проверки, и возвращается булево значение.
Правильный ответ:
Type Guard — это механизм сужения типа (type narrowing), который позволяет TypeScript определить более конкретный тип переменной в определённой ветке кода. Функция type guard имеет особую сигнатуру возврата — parameter is Type — которая сообщает компилятору о результате проверки.
Пользовательские type guard функции:
interface Cat {
type: 'cat';
meow(): void;
}
interface Dog {
type: 'dog';
bark(): void;
}
type Pet = Cat | Dog;
// Type Guard с сигнаурой возврата "pet is Cat"
function isCat(pet: Pet): pet is Cat {
return pet.type === 'cat';
}
function makeSound(pet: Pet) {
if (isCat(pet)) {
pet.meow(); // TypeScript знает, что здесь pet === Cat
} else {
pet.bark(); // TypeScript знает, что здесь pet === Dog
}
}
Ключевой момент — pet is Cat вместо : boolean. Это и делает функцию type guard. Без этой сигнатуры TypeScript не сможет сузить тип.
Встроенные механизмы type narrowing:
typeof guard:
function process(value: string | number) {
if (typeof value === 'string') {
value.toUpperCase(); // OK — TypeScript знает, что это string
} else {
value.toFixed(2); // OK — TypeScript знает, что это number
}
}
instanceof guard:
function handleError(error: unknown) {
if (error instanceof TypeError) {
console.log(error.message); // OK — TypeScript знает, что это TypeError
} else if (error instanceof Error) {
console.log(error.stack); // OK — TypeScript знает, что это Error
}
}
in operator guard:
function process(data: Cat | Dog) {
if ('meow' in data) {
data.meow(); // OK — TypeScript знает, что это Cat
} else {
data.bark(); // OK — TypeScript знает, что это Dog
}
}
Discriminated unions — самый удобный паттерн:
type ApiResponse =
| { status: 'success'; data: User[] }
| { status: 'error'; error: string }
| { status: 'loading' };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'success':
console.log(response.data); // OK — TypeScript знает тип
break;
case 'error':
console.log(response.error); // OK — TypeScript знает тип
break;
case 'loading':
console.log('Loading...'); // response не имеет data или error
break;
}
}
Assertion functions (утверждающие функции):
Type guard сужают тип внутри if, но assertion functions выбрасывают ошибку, если условие не выполнено:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function process(value: unknown) {
assertIsString(value);
// После вызова assertion function TypeScript знает, что value === string
value.toUpperCase(); // OK
}
Assertion без выброса ошибки — просто сужение:
function assertDefined<T>(value: T | undefined): asserts value is T {
if (value === undefined) {
throw new Error('Value is undefined');
}
}
function getUserName(id: number): string | undefined {
// ...
}
const name = getUserName(42);
assertDefined(name);
console.log(name.toUpperCase()); // OK — TypeScript знает, что name !== undefined
Практический пример — типобезопасный парсер API-ответа:
interface User {
id: number;
name: string;
email: string;
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof (data as Record<string, unknown>).id === 'number' &&
'name' in data &&
typeof (data as Record<string, unknown>).name === 'string' &&
'email' in data &&
typeof (data as Record<string, unknown>).email === 'string'
);
}
function isUserArray(data: unknown): data is User[] {
return Array.isArray(data) && data.every(isUser);
}
async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
const data: unknown = await response.json();
if (!isUserArray(data)) {
throw new Error('Invalid response format');
}
return data; // TypeScript знает, что это User[]
}
Рекомендации:
- Используйте discriminated unions, где возможно — это самый чистый и безопасный паттерн.
- Type guard функции выносите в отдельные утилиты, если проверка используется в нескольких местах.
- Для сложных проверок рассмотрите библиотеки валидации с поддержкой типов:
zod,io-ts,valibot— они автоматически создают type guard из схемы.
Вопрос 37. Что такое SOLID? Расскажите о каждом принципе.
Таймкод: 00:39:21
Ответ собеседника: правильный. S — Single Responsibility Principle: каждый класс должен выполнять одну задачу, все его сервисы направлены на обеспечение этой обязанности. O — Open/Closed Principle: можно добавлять новый функционал, не изменяя существующий код. L — Liskov Substitution Principle: должна быть взаимозаменяемость между классами, наследующий класс должен дополнять, а не изменять базовый. I — Interface Segregation Principle: лучше создавать много интерфейсов, чем один общий; классы не должны реализовывать методы, которые они не используют. D — Dependency Inversion Principle: высокоуровневые модули не должны зависеть от модулей более низкого уровня; оба типа модулей должны зависеть от абстракций (интерфейсов, абстрактных классов), а не от конкретных функций.
Правильный ответ:
SOLID — это пять принципов объектно-ориентированного проектирования, введённых Робертом Мартином, которые делают код более поддерживаемым, расширяемым и тестируемым.
S — Single Responsibility Principle (Принцип единственной ответственности)
Класс (или модуль, функция) должен иметь только одну причину для изменения — одну ответственность:
// Нарушение SRP: класс отвечает за всё подряд
class User {
saveToDatabase() { /* ... */ }
sendEmail() { /* ... */ }
generateReport() { /* ... */ }
validatePassword() { /* ... */ }
}
// Правильно: каждая ответственность в своём классе
class User {
name: string;
email: string;
}
class UserRepository {
save(user: User) { /* ... */ }
findById(id: string): User { /* ... */ }
}
class EmailService {
sendWelcomeEmail(user: User) { /* ... */ }
}
class ReportGenerator {
generateUserReport(user: User) { /* ... */ }
}
В контексте React это означает: компонент отвечает за рендеринг, хук — за логику, сервис — за работу с API. Не нужно смешивать всё в одном месте.
O — Open/Closed Principle (Принцип открытости/закрытости)
Модули должны быть открыты для расширения, но закрыты для модификации. Новое поведение добавляется через новый код, а не изменение существующего:
// Нарушение: при добавлении нового типа скидки нужно менять существующий метод
class DiscountCalculator {
calculate(type: string, amount: number): number {
if (type === 'percentage') return amount * 0.9;
if (type === 'fixed') return amount - 10;
if(type === 'buy_one_get_one') return amount / 2;
// Каждый новый тип = изменение этого метода
}
}
// Правильно: расширение через добавление новых классов
interface DiscountStrategy {
calculate(amount: number): number;
}
class PercentageDiscount implements DiscountStrategy {
calculate(amount: number) { return amount * 0.9; }
}
class FixedDiscount implements DiscountStrategy {
calculate(amount: number) { return amount - 10; }
}
class BuyOneGetOneDiscount implements DiscountStrategy {
calculate(amount: number) { return amount / 2; }
}
class DiscountCalculator {
constructor(private strategy: DiscountStrategy) {}
calculate(amount: number): number {
return this.strategy.calculate(amount);
}
}
// Новый тип скидки — просто новый класс, существующий код не меняется
class SeasonalDiscount implements DiscountStrategy {
calculate(amount: number) { return amount * 0.8; }
}
L — Liskov Substitution Principle (Принцип подстановки Лисков)
Объекты подкласса должны быть заменимы объектами суперкласса без нарушения корректности программы. Наследник должен дополнять, а не заменять поведение:
// Нарушение: Square меняет поведение Rectangle
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(width: number) { this.width = width; }
setHeight(height: number) { this.height = height; }
getArea() { return this.width * this.height; }
}
class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}
setWidth(width: number) {
this.width = width;
this.height = width; // Нарушение: изменяет инвариант Rectangle
}
setHeight(height: number) {
this.width = height;
this.height = height;
}
}
// Тест провалится для Square
function testRectangle(rect: Rectangle) {
rect.setWidth(5);
rect.setHeight(4);
console.log(rect.getArea()); // Ожидаем 20, но Square вернёт 16
}
// Правильно: не наследовать Square от Rectangle
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea() { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
getArea() { return this.side * this.side; }
}
I — Interface Segregation Principle (Принцип разделения интерфейса)
Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше много маленьких интерфейсов, чем один большой:
// Нарушение: один огромный интерфейс
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
writeCode(): void;
designUI(): void;
}
// JuniorDesigner вынужден реализовывать writeCode, хотя не пишет код
// Правильно: разделение на маленькие интерфейсы
interface Workable { work(): void }
interface Eatable { eat(): void }
interface Sleepable { sleep(): void }
interface MeetingAttendee { attendMeeting(): void }
interface Coder { writeCode(): void }
interface UIDesigner { designUI(): void }
class SeniorDeveloper implements Workable, Eatable, MeetingAttendee, Coder {
work() { /* ... */ }
eat() { /* ... */ }
attendMeeting() { /* ... */ }
writeCode() { /* ... */ }
}
class UIDesigner implements Workable, Eatable, MeetingAttendee, UIDesigner {
work() { /* ... */ }
eat() { /* ... */ }
attendMeeting() { /* ... */ }
designUI() { /* ... */ }
}
D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Высокоуровневые модули не должны зависеть от низкоуровневых. Оба должны зависеть от абстракций:
// Нарушение: NotificationService напрямую зависит от EmailSender
class EmailSender {
send(to: string, message: string) {
console.log(`Email to ${to}: ${message}`);
}
}
class NotificationService {
private emailSender = new EmailSender(); // Жёсткая привязка к конкретной реализации
notify(user: User, message: string) {
this.emailSender.send(user.email, message);
}
}
// Проблема: чтобы добавить SMS или push — нужно менять NotificationService
// Правильно: зависимость от абстракции
interface MessageSender {
send(to: string, message: string): void;
}
class EmailSender implements MessageSender {
send(to: string, message: string) {
console.log(`Email to ${to}: ${message}`);
}
}
class SmsSender implements MessageSender {
send(to: string, message: string) {
console.log(`SMS to ${to}: ${message}`);
}
}
class PushSender implements MessageSender {
send(to: string, message: string) {
console.log(`Push to ${to}: ${message}`);
}
}
class NotificationService {
constructor(private sender: MessageSender) {} // Зависим от абстракции
notify(user: User, message: string) {
this.sender.send(user.email, message);
}
}
// Легко подменить реализацию:
const emailNotifier = new NotificationService(new EmailSender());
const smsNotifier = new NotificationService(new SmsSender());
const pushNotifier = new NotificationService(new PushSender());
Важно понимать: SOLID — это не догма, а руководство. Слепое следование всем принципам может привести к избыточной абстракции и усложнению кода. Принципы применяются там, где они приносят пользу — когда код растёт, меняется и требует поддержки. Для простых утилит и скриптов полное соблюдение SOLID может быть избыточным.
Вопрос 38. Какие нововведения появились в React 19?
Таймкод: 00:41:59
Ответ собеседника: неполный. Упомянут React Compiler (который должен помочь избавиться от мемоизации), но он ещё не добавлен в релиз, находится в бете. Также упомянуты новые хуки (названия не вспомнил) и улучшения серверных компонентов (которые появились в React 18).
Правильный ответ:
React 19 (выпущен в декабре 2024) принёс значительные нововведения. Рассмотрим основные.
1. React Compiler (экспериментальный)
Автоматическая мемоизация — компилятор анализирует код и автоматически применяет мемоизацию там, где она нужна, избавляя разработчика от ручного использования useMemo, useCallback и React.memo:
// До React 19: ручная мемоизация
const MemoizedChild = React.memo(function Child({ data, onUpdate }) {
const processed = useMemo(() => heavyProcess(data), [data]);
const handleClick = useCallback(() => onUpdate(processed), [processed, onUpdate]);
return <button onClick={handleClick}>{processed}</button>;
});
// С React Compiler: обычный код, мемоизация автоматическая
function Child({ data, onUpdate }) {
const processed = heavyProcess(data); // Автоматически мемоизировано
const handleClick = () => onUpdate(processed); // Автоматически стабилизировано
return <button onClick={handleClick}>{processed}</button>;
}
Компилятор находится в бете и интегрирован в Meta (Facebook, Instagram). Для production рекомендуется использовать через Babel-плагин или интеграцию со сборщиком.
2. Actions — новый паттерн для асинхронных операций
Actions — это новый способ обработки асинхронных операций, управления состоянием ожидания, ошибками и оптимистичными обновлениями:
function UpdateName() {
const [name, setName] = useState('');
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect('/profile');
});
};
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={handleSubmit} disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}
3. Новые хуки
useActionState — управление состоянием формы:
function ChangeName({ currentName }) {
const [state, formAction, isPending] = useActionState(
async (previousState, formData) => {
const newName = formData.get('name');
const error = await updateName(newName);
if (error) return { error, name: previousState.name };
return { error: null, name: newName };
},
{ error: null, name: currentName }
);
return (
<form action={formAction}>
<input type="text" name="name" defaultValue={state.name} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
useOptimistic — оптимистичные обновления:
function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { text: newMessage, sending: true }]
);
async function sendMessage(formData) {
const message = formData.get('message');
addOptimisticMessage(message); // Мгновенно добавляем в UI
await deliverMessage(message); // Отправляем на сервер
}
return (
<>
{optimisticMessages.map((msg, i) => (
<div key={i} className={msg.sending ? 'pending' : ''}>
{msg.text}
</div>
))}
<form action={sendMessage}>
<input name="message" />
<button type="submit">Send</button>
</form>
</>
);
}
useFormStatus — доступ к статусу формы из дочернего компонента:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={submitHandler}>
<input name="email" />
<SubmitButton /> {/* Получает pending из ближайшей формы */}
</form>
);
}
use() — хук для чтения ресурсов (Promise, Context) прямо в рендере:
import { use } from 'react';
const themes = {
light: { fg: '#000', bg: '#fff' },
dark: { fg: '#fff', bg: '#000' },
};
const ThemeContext = createContext(themes.light);
function ThemedButton({ themePromise }) {
// use() приостанавливает компонент до разрешения Promise
const theme = use(themePromise);
// Или чтение контекста:
// const theme = use(ThemeContext);
return <button style={{ color: theme.fg, background: theme.bg }}>Click</button>;
}
4. Server Components (стабилизация)
Серверные компоненты, появившиеся в React 18 как экспериментальная функция, в React 19 стабилизированы:
// Server Component (выполняется только на сервере)
async function BlogPost({ id }: { id: string }) {
const post = await db.posts.findById(id); // Прямой доступ к БД, без API
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={id} /> {/* Client Component */}
</article>
);
}
// Client Component — нужна интерактивность
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}
5. Server Actions
Асинхронные функции, выполняемые на сервере, вызываемые из клиентского кода:
// Server Action
'use server';
async function createTodo(formData: FormData) {
const title = formData.get('title') as string;
await db.todos.create({ title });
revalidatePath('/todos');
}
// Использование в компоненте
function TodoForm() {
return (
<form action={createTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
);
}
6. Улучшения ref
ref теперь можно передавать как обычный проп — не нужен forwardRef:
// До React 19
const Input = React.forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
// React 19
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
7. Метаданные документа
Встроенная поддержка <title>, <meta>, <link> прямо в компонентах:
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
8. Web Assets
Поддержка загрузки стилей и скриптов с учётом приоритета:
function Component() {
return (
<>
<Suspense fallback="Loading...">
<Stylesheet href="/styles/article.css" />
<Article />
</Suspense>
</>
);
}
Итого: React 19 фокусируется на упрощении работы с асинхронными операциями (Actions, useActionState, useOptimistic), автоматической оптимизации (React Component) и серверным рендерингом (Server Components, Server Actions).
Вопрос 39. Что такое оптимистические обновления и в каких кейсах их лучше использовать?
Таймкод: 00:43:23
Ответ собеседника: правильный. Оптимистические обновления — это техника, при которой пользователь сразу видит обновление стейта, не дожидаясь ответа от бэкенда. Подходит для лайков/дизлайков в соцсетях, удаления элементов. Не подходит для критичных данных (например, редактирование ячеек таблицы), потому что при ошибке данные откатятся назад, что расстроит пользователя.
Правильный ответ:
Оптимистические обновления — это паттерн, при котором интерфейс обновляется сразу после действия пользователя, не дожидаясь подтверждения от сервера. Если запрос завершается неудачей — состояние откатывается к предыдущему.
Как это работает:
Пользователь нажимает "Лайк"
→ UI сразу показывает "лайкнуто" (оптимистичное обновление)
→ Отправляется запрос на сервер
→ Успех: всё хорошо, UI остаётся актуальным
→ Ошибка: UI откатывается к состоянию "не лайкнуто", показывается уведомление
Реализация без React 19:
function LikeButton({ postId, initialLikes, initialLiked }) {
const [liked, setLiked] = useState(initialLiked);
const [likesCount, setLikesCount] = useState(initialLikes);
const [error, setError] = useState(null);
const handleLike = async () => {
// Сохраняем предыдущее состояние для отката
const previousLiked = liked;
const previousCount = likesCount;
// Оптимистичное обновление
setLiked(!liked);
setLikesCount(liked ? likesCount - 1 : likesCount + 1);
setError(null);
try {
await api.toggleLike(postId);
} catch (err) {
// Откат при ошибке
setLiked(previousLiked);
setLikesCount(previousCount);
setError('Failed to update like');
}
};
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'} {likesCount}
{error && <span className="error">{error}</span>}
</button>
);
}
Реализация с React 19 (useOptimistic):
function LikeButton({ postId, initialLikes, initialLiked }) {
const [liked, setLiked] = useState(initialLiked);
const [likesCount, setLikesCount] = useState(initialLikes);
const [optimisticState, addOptimistic] = useOptimistic(
{ liked, likesCount },
(state, newLiked: boolean) => ({
liked: newLiked,
likesCount: newLiked ? state.likesCount + 1 : state.likesCount - 1,
})
);
const handleLike = async () => {
const newLiked = !liked;
addOptimistic(newLiked); // Оптимистичное обновление
try {
await api.toggleLike(postId);
setLiked(newLiked);
setLikesCount(prev => newLiked ? prev + 1 : prev - 1);
} catch {
// useOptimistic автоматически откатит состояние
}
};
return (
<button onClick={handleLike}>
{optimisticState.liked ? '❤️' : '🤍'} {optimisticState.likesCount}
</button>
);
}
Когда использовать оптимистические обновления:
- Лайки/дизлайки, добавление в избранное, подписки — действия с высокой вероятностью успеха, откат не критичен.
- Удаление элементов из списка — элемент исчезает мгновенно, при ошибке возвращается.
- Переключение флажков (toggle) — включение/выключение настроек, статусов.
- Отправка комментариев, сообщений — сообщение появляется сразу, при ошибке показывается уведомление.
- Drag-and-drop переупорядочивание — элемент перемещается сразу, при ошибке возвращается на место.
Когда НЕ использовать:
- Финансовые операции — переводы, оплата. Откат после «успешного» действия вызывает панику.
- Критичные данные — редактирование медицинских записей, юридических документов.
- Действия с необратимыми последствиями — удаление аккаунта, публикация в печать.
- Формы с валидацией на сервере — если сервер может отклонить данные по сложным правилам, лучше дождаться ответа.
- Нестабильное соединение — если частота ошибок высока, постоянные откаты раздражают пользователя.
Паттерн «отложенная оптимизация»:
Для случаев, где оптимистичное обновление желательно, но риск ошибки значителен, используется гибридный подход — мгновенная индикация без изменения данных:
function SaveButton({ data }) {
const [saving, setSaving] = useState(false);
const handleSave = async () => {
setSaving(true); // Показываем "Сохранение..." без изменения данных
try {
await api.save(data);
showNotification('Saved!');
} catch {
showError('Save failed');
} finally {
setSaving(false);
}
};
return <button disabled={saving}>{saving ? 'Saving...' : 'Save'}</button>;
}
Это не оптимистичное обновление в чистом виде, но даёт пользователю мгновенную обратную связь без риска отката.
Вопрос 40. На какие три этапа делится жизненный цикл функционального компонента в React и что является триггером для обновления?
Таймкод: 00:45:52
Ответ собеседника: неполный. Три этапа: монтирование, обновление, размонтирование. Триггером для обновления является изменение стейта.
Правильный ответ:
Три этапа жизненного цикла:
1. Монтирование (Mounting)
Компонент создаётся и впервые добавляется в DOM. Вызывается тело функционального компонента, затем эффекты:
function MyComponent() {
// Этап монтирования: компонент рендерится впервые
const [data, setData] = useState(null); // Инициализация состояния
useEffect(() => {
// Выполняется после первого рендера (монтирования)
console.log('Component mounted');
fetchData().then(setData);
return () => {
// Функция очистки — выполнится при размонтировании
console.log('Component unmounted');
};
}, []); // Пустой массив зависимостей = выполнить один раз при монтировании
return <div>{data ? data.name : 'Loading...'}</div>;
}
2. Обновление (Updating)
Компонент перерисовывается. Происходит повторный вызов тела функции, затем эффекты (после очистки предыдущих):
function MyComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Выполняется при монтировании и при каждом изменении userId
fetchUser(userId).then(setUser);
}, [userId]); // userId в зависимостях
// Каждый рендер — это вызов этой функции с актуальными пропсами и состоянием
return <div>{user?.name}</div>;
}
3. Размонтирование (Unmounting)
Компонент удаляется из DOM. Выполняются функции очистки из эффектов:
function MyComponent() {
useEffect(() => {
const subscription = eventBus.subscribe('update', handler);
return () => {
// Выполняется при размонтировании
subscription.unsubscribe();
};
}, []);
return <div>Content</div>;
}
Триггеры обновления (ре-рендеров):
Изменение состояния — не единственный триггер. Ре-рендер происходит по нескольким причинам:
1. Изменение состояния (useState, useReducer)
const [count, setCount] = useState(0);
setCount(1); // Триггер ре-рендера
2. Изменение пропсов от родительского компонента
// Родитель перерисовывается → передаёт новые пропсы → ребёнок перерисовывается
function Parent() {
const [theme, setTheme] = useState('light');
return <Child theme={theme} />; // Изменение theme → ре-рендер Child
}
3. Ре-рендер родительского компонента
Даже если пропсы не изменились, ребёнок перерисовывается при ре-рендере родителя (если не применена мемоизация):
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child name="Alice" /> {/* Child перерисовывается при каждом клике, хотя name не меняется */}
</div>
);
}
// Решение: React.memo
const Child = React.memo(function Child({ name }) {
return <div>{name}</div>;
});
4. Изменение контекста
Если значение в Context.Provider изменилось, все компоненты, подписанные на этот контекст через useContext, перерисуются:
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); // Ре-рендер при изменении theme
return <button className={theme}>Click</button>;
}
5. Принудительный ре-рендер (forceUpdate)
В функциональных компонентах нет прямого forceUpdate, но его можно эмулировать:
const [, forceRender] = useState({});
forceRender({}); // Принудительный ре-рендер
Важно: React автоматически батчит обновления внутри обработчиков событий и эффектов. В React 18+ батчинг работает везде, включая промисы и setTimeout:
function handleClick() {
setCount(c => c + 1); // Не вызывает немедленный рендер
setFlag(f => !f); // Не вызывает немедленный рендер
// Оба обновления применятся за один ре-рендер
}
Вопрос 41. Что является триггером для обновления компонента в React?
Таймкод: 00:46:13
Ответ собеседника: неполный. Названы: изменение стейта, изменение ключей, изменение контекста, рендер родителя и изменение пропсов. При этом все сведены к изменению стейта, что не совсем корректно — контекст и пропсы являются отдельными случаями, а рендер родителя не всегда влечёт рендер ребёнка (если ребёнок обёрнут в React.memo или задан ключ).
Правильный ответ:
Ре-рендер компонента в React может быть вызван несколькими независимыми причинами. Рассмотрим каждую.
1. Изменение внутреннего состояния
Самый очевидный триггер — вызов setState или dispatch в useReducer:
const [count, setCount] = useState(0);
setCount(1); // Ре-рендер
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'INCREMENT' }); // Ре-рендер
2. Изменение пропсов от родителя
Когда родитель передаёт новые пропсы, дочерний компонент перерисовывается:
function Parent() {
const [theme, setTheme] = useState('light');
return <Child theme={theme} />;
}
function Child({ theme }) {
// Ре-рендер при каждом изменении theme в Parent
return <div className={theme}>Content</div>;
}
3. Ре-рендер родительского компонента
По умолчанию, когда родитель перерисовывается, все его дочерние компоненты тоже перерисовываются — даже если их пропсы не изменились:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild /> {/* Перерисовывается при каждом клике, хотя не получает пропсов */}
</div>
);
}
Это поведение можно предотвратить через React.memo:
const ExpensiveChild = React.memo(function ExpensiveChild() {
// Не перерисовывается при ре-рендере Parent
return <div>Heavy content</div>;
});
4. Изменение значения контекста
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext); // Подписка на контекст
// Ре-рендер при изменении значения ThemeContext.Provider
return <button className={theme}>Click</button>;
}
5. Изменение ключа (key)
Ключ — это специальный проп, который React использует для идентификации элемента в списке. При изменении ключа React полностью размонтирует старый компонент и создаст новый:
function SearchResults({ query }) {
return (
<div>
{results.map(item => (
<ResultCard key={item.id} item={item} />
))}
{/* При изменении query с новыми результатами:
элементы с новыми item.id — монтируются,
элементы с удалёнными item.id — размонтируются,
элементы с существующими item.id — обновляются */}
</div>
);
}
// Принудительный сброс состояния через key:
function Form({ userId }) {
return <UserProfile key={userId} userId={userId} />;
// При изменении userId — UserProfile полностью пересоздаётся с чистого состояния
}
Итого, триггеры ре-рендера:
| Триггер | Описание | Можно предотвратить |
|---|---|---|
setState / dispatch | Изменение внутреннего состояния | Нет |
| Изменение пропсов | Родитель передал новые значения | React.memo |
| Ре-рендер родителя | Родитель перерисовался | React.memo |
| Изменение контекста | Изменилось значение в Provider | Разделение контекстов |
| Изменение key | Новый ключ = новый компонент | Не применимо (это фича) |
Важно понимать: сам по себе ре-рендер — не проблема. React эффективно обрабатывает ре-рендеры. Проблема возникает, когда ре-рендер запускает дорогие вычисления или ненужные побочные эффекты. Именно для этого существуют React.memo, useMemo, useCallback — но они должны применяться осознанно, на основе профилирования, а не «на всякий случай».
Вопрос 42. Что такое Force Update и как его реализовать на функциональных компонентах?
Таймкод: 00:48:38
Ответ собеседника: неполный. Force Update — это метод this.forceUpdate() в классовых компонентах. На функциональных компонентах его можно реализовать через useReducer, как описано в документации React.
Правильный ответ:
Что такое Force Update
Force Update — это принудительный ре-рендер компонента, который вызывается в обход обычных триггеров (состояние, пропсы, контекст). В классовых компонентах для этого есть метод this.forceUpdate():
class MyComponent extends React.Component {
handleForceUpdate = () => {
this.forceUpdate(); // Принудительный ре-рендер
};
render() {
return <button onClick={this.forceUpdate}>Rerender</button>;
}
}
Зачем это может быть нужно:
- Компонент зависит от внешних данных, которые React не отслеживает (мутабельные объекты, глобальные переменные, DOM-состояние вне React).
- Интеграция с не-React библиотеками, которые мутируют DOM напрямую.
- Редкие edge cases, когда нужно принудительно синхронизировать UI.
Реализация на функциональных компонентах:
В функциональных компонентах нет forceUpdate(), но его можно эмулировать через useReducer или useState:
Способ 1: useReducer (рекомендуемый)
function useForceUpdate() {
const [, forceUpdate] = useReducer((x) => x + 1, 0);
return forceUpdate;
}
function MyComponent() {
const forceUpdate = useForceUpdate();
return (
<button onClick={forceUpdate}>
Force Update
</button>
);
}
useReducer с функцией (x) => x + 1 гарантирует, что каждый вызов forceUpdate создаёт новое значение, что вызывает ре-рендер.
Способ 2: useState с объектом
function useForceUpdate() {
const [, setState] = useState({});
return () => setState({});
}
function MyComponent() {
const forceUpdate = useForceUpdate();
return <button onClick={forceUpdate}>Force Update</button>;
}
Новый объект {} при каждом вызове всегда отличается по ссылке, что триггерит ре-рендер.
Способ 3: useState с противоречивым значением
function useForceUpdate() {
const [value, setValue] = useState(false);
return () => setValue((prev) => !prev);
}
Когда использовать:
// Пример: компонент зависит от внешнего мутабельного объекта
let externalData = { count: 0 };
function Counter() {
const forceUpdate = useForceUpdate();
const increment = () => {
externalData.count += 1; // Мутация — React не видит изменение
forceUpdate(); // Принудительный ре-рендер
};
return (
<div>
<p>Count: {externalData.count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Важно: forceUpdate — это антипаттерн в большинстве случаев. Если вы используете его, скорее всего, есть архитектурная проблема: состояние должно храниться в React, а не во внешних мутабельных объектах. Перед применением forceUpdate стоит рассмотреть:
- Перенос данных в
useState/useReducer. - Использование подписки на внешний стор (Zustand, Redux).
- Использование
useSyncExternalStore(React 17+) для интеграции с внешними источниками данных:
import { useSyncExternalStore } from 'react';
function useExternalStore(subscribe, getSnapshot) {
return useSyncExternalStore(subscribe, getSnapshot);
}
// Это правильный способ работы с внешними мутабельными данными
Вопрос 43. Что такое стадия размонтирования компонента и что на ней нужно делать?
Таймкод: 00:49:32
Ответ собеседника: правильный. На стадии размонтирования нужно отписываться от событий и отправлять метрики, чтобы избежать утечек памяти.
Правильный ответ:
Стадия размонтирования (Unmounting) — это момент, когда компонент удаляется из DOM. В функциональных компонентах это соответствует выполнению функций очистки (cleanup), возвращаемых из useEffect и useLayoutEffect.
Что нужно делать при размонтировании:
1. Отписка от внешних событий и подписок
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
const handleMessage = (msg) => {
// Обработка сообщения
};
connection.on('message', handleMessage);
return () => {
// Очистка при размонтировании
connection.disconnect();
connection.off('message', handleMessage);
};
}, [roomId]);
}
2. Отмена таймеров и интервалов
function Timer() {
useEffect(() => {
const intervalId = setInterval(() => {
console.log('tick');
}, 1000);
const timeoutId = setTimeout(() => {
console.log('done');
}, 5000);
return () => {
clearInterval(intervalId);
clearTimeout(timeoutId);
};
}, []);
}
3. Отмена незавершённых запросов (AbortController)
function DataFetcher({ userId }) {
useEffect(() => {
const abortController = new AbortController();
fetch(`/api/users/${userId}`, { signal: abortController.signal })
.then(r => r.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
});
return () => {
abortController.abort(); // Отмена запроса при размонтировании
};
}, [userId]);
}
4. Отписка от глобальных событий (window, document)
function ScrollTracker() {
useEffect(() => {
const handleScroll = () => {
console.log(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
}
5. Очистка подписок на WebSocket, EventSource, BroadcastChannel
function LiveFeed() {
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/feed');
ws.onmessage = (event) => {
setData(JSON.parse(event.data));
};
return () => {
ws.close();
};
}, []);
}
6. Очистка IntersectionObserver, ResizeObserver, MutationObserver
function VisibleTracker({ targetRef }) {
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
setIsVisible(entry.isIntersecting);
});
});
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => {
observer.disconnect();
};
}, [targetRef]);
}
7. Отмена анимаций (requestAnimationFrame)
function AnimatedComponent() {
useEffect(() => {
let animationId;
const animate = () => {
// Логика анимации
animationId = requestAnimationFrame(animate);
};
animationId = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(animationId);
};
}, []);
}
8. Отписка от внешних сторов (Zustand, Redux, etc.)
function UserProfile({ userId }) {
useEffect(() => {
const unsubscribe = userStore.subscribe((state) => {
// Реакция на изменения
});
return () => {
unsubscribe();
};
}, [userId]);
}
Что произойдёт, если не делать очистку:
- Утечки памяти — подписчики продолжают ссылаться на размонтированный компонент.
- Ошибки в консоли — попытка обновить состояние размонтированного компонента (
Can't perform a React state update on an unmounted component). - Дублирование подписок — при повторном монтировании компонента создастся новая подписка, а старая не будет удалена.
Проверка на размонтирование (для async-операций):
function DataFetcher({ userId }) {
useEffect(() => {
let isMounted = true;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
if (isMounted) {
setData(data); // Обновляем состояние только если компонент ещё смонтирован
}
});
return () => {
isMounted = false;
};
}, [userId]);
}
Хотя использование isMounted считается антипаттерном в React 19 (вместо этого рекомендуется AbortController), оно по-прежнему встречается в старом коде. Современный подход — использование AbortController для отмены запросов, как показано выше.
Вопрос 44. Что такое Error Boundary и как он реализуется?
Таймкод: 00:49:55
Ответ собеседника: неполный. Error Boundary — компонент для отображения резервного контента при падении ошибки, чтобы избежать белого экрана. Реализуется через классовые компоненты с методами componentDidCatch и getDerivedStateFromError.
Правильный ответ:
Что такое Error Boundary
Error Boundary — это компонент-обёртка, который перехватывает JavaScript-ошибки в дочерних компонентах, логирует их и отображает fallback UI вместо падения всего приложения. Без Error Boundary любая необработанная ошибка в компоненте приводит к размонтированию всего дерева (в React 16+ — белый экран).
Почему только классовые компоненты
Error Boundary используют специфичные методы жизненного цикла — static getDerivedStateFromError() и componentDidCatch() — которых нет в функциональных компонентах и хуках. В функциональных компонентах нет прямого аналога, поэтому Error Boundary обязательно реализуется как классовый компонент.
Реализация Error Boundary:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
// Вызывается при ошибке в дочернем компоненте
// Обновляет состояние для отображения fallback UI
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Вызывается после ошибки — используется для логирования
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo });
// Отправка ошибки в сервис мониторинга
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Кастомный fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
// Стандартный fallback
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Вспомогательная функция для логирования
function logErrorToService(error, errorInfo) {
// Отправка в Sentry, Datadog, LogRocket и т.д.
console.error('Error caught by boundary:', error, errorInfo);
}
Использование:
function App() {
return (
<ErrorBoundary fallback={<p>App crashed</p>}>
<Header />
<ErrorBoundary fallback={<p>Main content failed</p>}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
Что перехватывает Error Boundary:
- Ошибки во время рендеринга.
- Ошибки в методах жизненного цикла.
- Ошибки в конструкторах дочерних компонентов.
Что НЕ перехватывает Error Boundary:
- Ошибки в обработчиках событий (onClick, onSubmit и т.д.) — используйте
try/catch. - Асинхронные ошибки (setTimeout, fetch, Promise) — используйте
try/catchв эффектах. - Ошибки в самом Error Boundary.
- Ошибки на сервере (SSR).
Обработка ошибок в обработчиках событий:
function MyComponent() {
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
// Error Boundary не поймает эту ошибку
setError(error.message);
}
};
return <button onClick={handleClick}>Do something</button>;
}
Библиотека react-error-boundary:
Для более удобной работы с Error Boundary существует библиотека react-error-boundary, которая упрощает создание и добавляет дополнительные возможности:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logErrorToService(error, info)}
onReset={() => {
// Сброс состояния при повторной попытке
resetApplicationState();
}}
>
<MainContent />
</ErrorBoundary>
);
}
Размещение Error Boundary:
- На уровне приложения — глобальный fallback для непредвиденных ошибок.
- На уровне страниц/маршрутов — изоляция ошибок по страницам (одна сломанная страница не ломает весь сайт).
- На уровне виджетов/блоков — изоляция некритичных блоков (виджет погоды, рекомендации, реклама) — если они упадут, остальная страница продолжит работать.
- На уровне отдельных компонентов — для критичных компонентов с нестабильными данными.
Вопрос 45. Что такое React Fiber и в чём его отличие от предыдущего движка?
Таймкод: 00:50:49
Ответ собеседника: правильный. React Fiber — это движок согласования, появившийся в React 16. Хранится в виде fiber nodes (связанного списка). Основное отличие — fiber-узлы не пересоздаются при каждом рендере и являются мутабельными.
Правильный ответ:
React Fiber — это полная переработка алгоритма reconciliation (согласования) в React, представленная в React 16. Название «Fiber» отражает концепцию «волокон» — мелких единиц работы, из которых строится рендеринг.
Проблема старого движка (Stack Reconciler)
До React 16 использовался Stack Reconciler, основанный на рекурсивном обходе дерева компонентов. У него было критическое ограничение — невозможность прервать работу:
Рекурсивный обход дерева:
render(App)
render(Header)
render(Nav)
render(Link) ← выполняется до конца
render(Link)
render(Search)
render(Main)
render(List)
render(Item) ← выполняется до конца
render(Item)
render(Item)
...сотни элементов...
Рекурсивный стек нельзя прервать — если React начал рендерить компонент, он обязан закончить весь поддерево. При большом дереве это блокировало основной поток на сотни миллисекунд, вызывая заметные фризы (frame drops).
Как работает Fiber
Fiber заменил рекурсивный стек на связный список мутабельных узлов. Каждый компонент представлен как Fiber-узел:
// Примерная структура Fiber-узла
const fiberNode = {
type: 'div', // Тип компонента
key: null,
props: { className: 'app' },
stateNode: domElement, // Ссылка на DOM-элемент
// Связи в дереве (связный список)
return: parentFiber, // Ссылка на родителя
child: firstChild, // Первый дочерний
sibling: nextSibling, // Следующий sibling
// Для reconciliation
alternate: oldFiber, // Ссылка на предыдущий fiber (для сравнения)
effectTag: 'UPDATE', // Тип изменения
};
Ключевое отличие — структура child → sibling → return вместо рекурсивного стека. Это позволяет React:
1. Разбить работу на единицы (units of work)
Каждый Fiber-узел — это отдельная единица работы. React обрабатывает один узел, затем проверяет, не истекло ли время кадра.
2. Приоритизировать обновления
Разные обновления имеют разный приоритет:
- Высокий приоритет — пользовательский ввод (клик, ввод текста), анимации.
- Низкий приоритет — загрузка данных, рендер невидимых элементов.
React может приостановить работу над низкоприоритетным обновлением, чтобы обработать высокоприоритетное.
3. Прерывать и возобновлять работу
Работа над Fiber-деревом:
1. Обрабатываем Fiber A (1ms) → проверяем время → осталось 15ms → продолжаем
2. Обрабатываем Fiber B (2ms) → проверяем → осталось 13ms → продолжаем
3. Обрабатываем Fiber C (0.5ms) → пришёл клик пользователя!
4. ПРЕРЫВАЕМ текущую работу
5. Обрабатываем клик (высокий приоритет)
6. ВОЗОБНОВЛЯЕМ работу с Fiber C
4. Отбрасывать устаревшую работу
Если пользователь ввёл новый символ, пока React рендерил предыдущий, старый рендер отбрасывается, и работа начинается заново с новым значением.
Две фазы работы Fiber:
Render phase (Render / Reconciliation) — асинхронная, прерываемая фаза:
- Обход Fiber-дерева.
- Вычисление изменений (diffing).
- Определение, какие узлы нужно обновить.
- Может быть прервана, приостановлена, отброшена.
Commit phase — синхронная, непрерываемая фаза:
- Применение изменений к реальному DOM.
- Вызов
useEffect,useLayoutEffect,componentDidMount/componentDidUpdate. - Выполняется за один проход — нельзя прервать.
Преимущества Fiber перед Stack Reconciler:
| Характеристика | Stack Reconciler | Fiber |
|---|---|---|
| Структура данных | Рекурсивный стек | Связный список |
| Прерываемость | Нет | Да |
| Приоритизация | Нет | Да |
| Отбрасывание работы | Нет | Да |
| Мутабельность | Новые объекты при каждом рендере | Мутабельные узлы, переиспользуются |
| Concurrent Mode | Невозможен | Основа для Concurrent Features |
Связь с Concurrent Mode:
Fiber — это фундамент, на котором построены Concurrent Features в React 18+ (useTransition, useDeferredValue, Suspense). Без прерываемого алгоритма Fiber эти возможности были бы невозможны — React не смог бы приостановить рендеринг, чтобы обработать более приоритетное обновление.
Вопрос 45. Что такое React Fiber и в чём его отличие от предыдущего движка?
Таймкод: 00:50:49
Ответ собеседника: правильный. React Fiber — это движок согласования, появившийся в React 16. Хранится в виде fiber nodes (связанного списка). Основное отличие — fiber-узлы не пересоздаются при каждом рендере и являются мутабельными.
Правильный ответ:
React Fiber — это полная переработка алгоритма reconciliation (согласования) в React, представленная в React 16. Название «Fiber» отражает концепцию «волокон» — мелких единиц работы, из которых строится рендеринг.
Проблема старого движка (Stack Reconciler)
До React 16 использовался Stack Reconciler, основанный на рекурсивном обходе дерева компонентов. У него было критическое ограничение — невозможность прервать работу:
Рекурсивный обход дерева:
render(App)
render(Header)
render(Nav)
render(Link) ← выполняется до конца
render(Link)
render(Search)
render(Main)
render(List)
render(Item) ← выполняется до конца
render(Item)
render(Item)
...сотни элементов...
Рекурсивный стек нельзя прервать — если React начал рендерить компонент, он обязан закончить весь поддерево. При большом дереве это блокировало основной поток на сотни миллисекунд, вызывая заметные фризы (frame drops).
Как работает Fiber
Fiber заменил рекурсивный стек на связный список мутабельных узлов. Каждый компонент представлен как Fiber-узел:
// Примерная структура Fiber-узла
const fiberNode = {
type: 'div', // Тип компонента
key: null,
props: { className: 'app' },
stateNode: domElement, // Ссылка на DOM-элемент
// Связи в дереве (связный список)
return: parentFiber, // Ссылка на родителя
child: firstChild, // Первый дочерний
sibling: nextSibling, // Следующий sibling
// Для reconciliation
alternate: oldFiber, // Ссылка на предыдущий fiber (для сравнения)
effectTag: 'UPDATE', // Тип изменения
};
Ключевое отличие — структура child → sibling → return вместо рекурсивного стека. Это позволяет React:
1. Разбить работу на единицы (units of work)
Каждый Fiber-узел — это отдельная единица работы. React обрабатывает один узел, затем проверяет, не истекло ли время кадра.
2. Приоритизировать обновления
Разные обновления имеют разный приоритет:
- Высокий приоритет — пользовательский ввод (клик, ввод текста), анимации.
- Низкий приоритет — загрузка данных, рендер невидимых элементов.
React может приостановить работу над низкоприоритетным обновлением, чтобы обработать высокоприоритетное.
3. Прерывать и возобновлять работу
Работа над Fiber-деревом:
1. Обрабатываем Fiber A (1ms) → проверяем время → осталось 15ms → продолжаем
2. Обрабатываем Fiber B (2ms) → проверяем → осталось 13ms → продолжаем
3. Обрабатываем Fiber C (0.5ms) → пришёл клик пользователя!
4. ПРЕРЫВАЕМ текущую работу
5. Обрабатываем клик (высокий приоритет)
6. ВОЗОБНОВЛЯЕМ работу с Fiber C
4. Отбрасывать устаревшую работу
Если пользователь ввёл новый символ, пока React рендерил предыдущий, старый рендер отбрасывается, и работа начинается заново с новым значением.
Две фазы работы Fiber:
Render phase (Render / Reconciliation) — асинхронная, прерываемая фаза:
- Обход Fiber-дерева.
- Вычисление изменений (diffing).
- Определение, какие узлы нужно обновить.
- Может быть прервана, приостановлена, отброшена.
Commit phase — синхронная, непрерываемая фаза:
- Применение изменений к реальному DOM.
- Вызов
useEffect,useLayoutEffect,componentDidMount/componentDidUpdate. - Выполняется за один проход — нельзя прервать.
Преимущества Fiber перед Stack Reconciler:
| Характеристика | Stack Reconciler | Fiber |
|---|---|---|
| Структура данных | Рекурсивный стек | Связный список |
| Прерываемость | Нет | Да |
| Приоритизация | Нет | Да |
| Отбрасывание работы | Нет | Да |
| Мутабельность | Новые объекты при каждом рендере | Мутабельные узлы, переиспользуются |
| Concurrent Mode | Невозможен | Основа для Concurrent Features |
Связь с Concurrent Mode:
Fiber — это фундамент, на котором построены Concurrent Features в React 18+ (useTransition, useDeferredValue, Suspense). Без прерываемого алгоритма Fiber эти возможности были бы невозможны — React не смог бы приостановить рендеринг, чтобы обработать более приоритетное обновление.
Вопрос 46. Какая ключевая фича появилась в React 18 и какую проблему она решает?
Таймкод: 00:52:31
Ответ собеседника: правильный. Automatic Batching (батчинг) — группировка нескольких вызовов обновления стейта в один рендер, чтобы избежать множества перерендеров при вызове нескольких setState внутри одной функции.
Правильный ответ:
React 18 принёс несколько ключевых нововведений. Рассмотрим основные.
1. Automatic Batching (Автоматический батчинг)
До React 18 батчинг (группировка нескольких setState в один рендер) работал только внутри обработчиков событий React. Вне их — в промисах, setTimeout, нативных обработчиках — каждый setState вызывал отдельный рендер:
// React 17: два рендера
const handleClick = () => {
setCount(c => c + 1); // Рендер 1
setFlag(f => !f); // Рендер 2
};
// React 17 внутри setTimeout: два рендера
setTimeout(() => {
setCount(c => c + 1); // Рендер 1
setFlag(f => !f); // Рендер 2
}, 1000);
// React 18: всегда один рендер (везде)
setTimeout(() => {
setCount(c => c + 1); // Батчится
setFlag(f => !f); // Батчится → один рендер
}, 1000);
Если нужно отключить батчинг в React 18 — используется flushSync:
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // Немедленный рендер
});
flushSync(() => {
setFlag(f => !f); // Немедленный рендер
});
2. Concurrent Features (Конкурентные возможности)
На основе Fiber-архитектуры появились новые API для управления приоритетами рендеринга:
useTransition — пометить обновление как низкоприоритетное:
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // Высокий приоритет — ввод текста мгновенный
startTransition(() => {
setResults(heavySearch(e.target.value)); // Низкий приоритет — может подождать
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
useDeferredValue — отложить обновление значения:
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// query обновляется мгновенно (ввод текста)
// deferredQuery обновляется с задержкой (тяжёлый поиск)
const results = useMemo(() => heavySearch(deferredQuery), [deferredQuery]);
return <ResultsList results={results} />;
}
3. Suspense для данных (экспериментальный)
В React 18 Suspense стабилизирован и может использоваться для отображения состояния загрузки данных:
function App() {
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userId={42} />
</Suspense>
);
}
// Компонент "приостанавливается" пока данные загружаются
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // use() + Suspense
return <div>{user.name}</div>;
}
4. New Hooks
useId — генерация уникальных ID для accessibility:
function Form() {
const id = useId();
return (
<div>
<label htmlFor={`${id}-name`}>Name</label>
<input id={`${id}-name`} />
</div>
);
}
useSyncExternalStore — безопасная подписка на внешние сторы:
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine
);
return isOnline;
}
useInsertionEffect — для CSS-in-JS библиотек (вставка стилей до мутатций DOM):
function useCSS(rule: string) {
useInsertionEffect(() => {
// Вставить стиль в DOM до того, как layout effects сработают
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
}, [rule]);
}
5. Strict Mode improvements
В React 18 Strict Mode двойно вызывает эффекты (mount → unmount → mount) для обнаружения проблем с очисткой:
// В режиме разработки Strict Mode:
// 1. Монтирует компонент
// 2. Выполняет эффекты
// 3. Размонтирует компонент
// 4. Выполняет cleanup эффектов
// 5. Монтирует компонент заново
// 6. Выполняет эффекты заново
Это помогает обнаружить утечки памяти и неправильную очистку на этапе разработки.
Итого, ключевые фичи React 18:
| Фича | Проблема |
|---|---|
| Automatic Batching | Лишние ре-рендеры вне обработчиков событий |
| useTransition | Блокировка UI при тяжёлых обновлениях |
| useDeferredValue | Отложенное обновление для неважных данных |
| Suspense для данных | Управление состоянием загрузки |
| Concurrent Rendering | Приоритизация обновлений для отзывчивого UI |
Вопрос 46. Какая ключевая фича появилась в React 18 и какую проблему она решает?
Таймкод: 00:52:31
Ответ собеседника: правильный. Automatic Batching (батчинг) — группировка нескольких вызовов обновления стейта в один рендер, чтобы избежать множества перерендеров при вызове нескольких setState внутри одной функции.
Правильный ответ:
React 18 принёс несколько ключевых нововведений. Рассмотрим основные.
1. Automatic Batching (Автоматический батчинг)
До React 18 батчинг (группировка нескольких setState в один рендер) работал только внутри обработчиков событий React. Вне их — в промисах, setTimeout, нативных обработчиках — каждый setState вызывал отдельный рендер:
// React 17: два рендера
const handleClick = () => {
setCount(c => c + 1); // Рендер 1
setFlag(f => !f); // Рендер 2
};
// React 17 внутри setTimeout: два рендера
setTimeout(() => {
setCount(c => c + 1); // Рендер 1
setFlag(f => !f); // Рендер 2
}, 1000);
// React 18: всегда один рендер (везде)
setTimeout(() => {
setCount(c => c + 1); // Батчится
setFlag(f => !f); // Батчится → один рендер
}, 1000);
Если нужно отключить батчинг в React 18 — используется flushSync:
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // Немедленный рендер
});
flushSync(() => {
setFlag(f => !f); // Немедленный рендер
});
2. Concurrent Features (Конкурентные возможности)
На основе Fiber-архитектуры появились новые API для управления приоритетами рендеринга:
useTransition — пометить обновление как низкоприоритетное:
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // Высокий приоритет — ввод текста мгновенный
startTransition(() => {
setResults(heavySearch(e.target.value)); // Низкий приоритет — может подождать
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</div>
);
}
useDeferredValue — отложить обновление значения:
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// query обновляется мгновенно (ввод текста)
// deferredQuery обновляется с задержкой (тяжёлый поиск)
const results = useMemo(() => heavySearch(deferredQuery), [deferredQuery]);
return <ResultsList results={results} />;
}
3. Suspense для данных (экспериментальный)
В React 18 Suspense стабилизирован и может использоваться для отображения состояния загрузки данных:
function App() {
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userId={42} />
</Suspense>
);
}
// Компонент "приостанавливается" пока данные загружаются
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // use() + Suspense
return <div>{user.name}</div>;
}
4. New Hooks
useId — генерация уникальных ID для accessibility:
function Form() {
const id = useId();
return (
<div>
<label htmlFor={`${id}-name`}>Name</label>
<input id={`${id}-name`} />
</div>
);
}
useSyncExternalStore — безопасная подписка на внешние сторы:
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine
);
return isOnline;
}
useInsertionEffect — для CSS-in-JS библиотек (вставка стилей до мутатций DOM):
function useCSS(rule: string) {
useInsertionEffect(() => {
// Вставить стиль в DOM до того, как layout effects сработают
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
}, [rule]);
}
5. Strict Mode improvements
В React 18 Strict Mode двойно вызывает эффекты (mount → unmount → mount) для обнаружения проблемы с очисткой:
// В режиме разработки Strict Mode:
// 1. Монтирует компонент
// 2. Выполняет эффекты
// 3. Размонтирует компонент
// 4. Выполняет cleanup эффектов
// 5. Монтирует компонент заново
// 6. Выполняет эффекты заново
Это помогает обнаружить утечки памяти и неправильную очистку на этапе разработки.
Итого, ключевые фичи React 18:
| Фича | Проблема |
|---|---|
| Automatic Batching | Лишние ре-рендеры вне обработчиков событий |
| useTransition | Блокировка UI при тяжёлых обновлениях |
| useDeferredValue | Отложенное обновление для неважных данных |
| Suspense для данных | Управление состоянием загрузки |
| Concurrent Rendering | Приоритизация обновлений для отзывчивого UI |
Вопрос 47. Проведите рефакторинг компонента с применением best practices: оптимизация импортов, названия компонента, деструктуризация пропсов, использование семантических тегов, оптимизация вычислений и обработчиков событий.
Таймкод: 00:54:54
Ответ собеседника: неполный. В ходе рефакторинга были выполнены: исправление импортов, переименование компонента с большой буквы, деструктуризация пропсов, удаление лишнего React-импорта и фрагмента, замена div на li с заданием ключей, вынесение неиспользуемого состояния items в константу, добавление обработчика событий с removeEventListener и вынесением функции в отдельную переменную с массивом зависимостей в useCallback. Также предложено вынести независимую функцию в отдельный файл и использовать делегирование событий вместо создания отдельного обработчика onClick для каждого элемента списка (через data-атрибуты и event.target).
Правильный ответ:
Рассмотрим типичный «до» и «после» рефакторинг компонента с применением best practices.
До рефакторинга (типичный «проблемный» компонент):
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { fetchUsers, formatDate, validateEmail } from '../../utils/helpers';
import './styles.css';
const myComponent = (props) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filter, setFilter] = useState('');
useEffect(() => {
const loadData = async () => {
try {
const data = await fetchUsers();
setItems(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadData();
}, []);
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
const handleItemClick = (id) => {
console.log('Clicked:', id);
};
const handleInputChange = (e) => {
setFilter(e.target.value);
};
return (
<React.Fragment>
<div className="container">
<div className="header">
<div className="title">{props.title}</div>
<div className="subtitle">{props.subtitle}</div>
</div>
<div className="search">
<input
type="text"
value={filter}
onChange={handleInputChange}
placeholder="Search..."
/>
</div>
{loading && <div>Loading...</div>}
{error && <div className="error">{error}</div>}
<div className="list">
{filteredItems.map(item => (
<div
className="item"
key={item.id}
onClick={() => handleItemClick(item.id)}
>
<div className="item-name">{item.name}</div>
<div className="item-email">{item.email}</div>
<div className="item-date">{formatDate(item.createdAt)}</div>
</div>
))}
</div>
</div>
</React.Fragment>
);
};
export default myComponent;
После рефакторинга:
import { useState, useEffect, useCallback, useMemo } from 'react';
import { fetchUsers, formatDate } from '../../utils/helpers';
import styles from './UserList.module.css';
// Типизация пропсов
interface UserListProps {
title: string;
subtitle?: string;
onItemClick?: (id: string) => void;
}
// Вынесение чистой функции за пределы компонента
const filterItems = (items: User[], filter: string): User[] => {
if (!filter.trim()) return items;
const lowerFilter = filter.toLowerCase();
return items.filter(item => item.name.toLowerCase().includes(lowerFilter));
};
// Вынесение обработчика клика на уровень родителя (event delegation)
function UserList({ title, subtitle, onItemClick }: UserListProps) {
const [items, setItems] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState('');
// Загрузка данных
useEffect(() => {
let isMounted = true;
const loadData = async () => {
try {
const data = await fetchUsers();
if (isMounted) {
setItems(data);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
loadData();
return () => {
isMounted = false;
};
}, []);
// Мемоизация фильтрации
const filteredItems = useMemo(
() => filterItems(items, filter),
[items, filter]
);
// Мемоизация обработчика ввода
const handleFilterChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setFilter(e.target.value);
},
[]
);
// Делегирование событий — один обработчик на список
const handleListClick = useCallback(
(e: React.MouseEvent<HTMLUListElement>) => {
const target = e.target as HTMLElement;
const itemEl = target.closest('[data-item-id]');
if (itemEl) {
const id = itemEl.getAttribute('data-item-id');
if (id) onItemClick?.(id);
}
},
[onItemClick]
);
// Ранние возвраты для состояний загрузки/ошибки
if (loading) {
return <p className={styles.loading}>Loading...</p>;
}
if (error) {
return <p className={styles.error} role="alert">{error}</p>;
}
return (
<section className={styles.container}>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
{subtitle && <p className={styles.subtitle}>{subtitle}</p>}
</header>
<input
type="search"
className={styles.searchInput}
value={filter}
onChange={handleFilterChange}
placeholder="Search users..."
aria-label="Search users"
/>
<ul className={styles.list} onClick={handleListClick}>
{filteredItems.map(item => (
<li
key={item.id}
className={styles.item}
data-item-id={item.id}
role="button"
tabIndex={0}
>
<span className={styles.itemName}>{item.name}</span>
<span className={styles.itemEmail}>{item.email}</span>
<time className={styles.itemDate} dateTime={item.createdAt}>
{formatDate(item.createdAt)}
</time>
</li>
))}
</ul>
<footer className={styles.footer}>
<p>{filteredItems.length} of {items.length} users</p>
</footer>
</section>
);
}
export default UserList;
Что было исправлено и почему:
1. Импорты
- Убран
import React— с React 17+ не нужен при использовании JSX Transform. - Убраны неиспользуемые импорты (
validateEmail). - CSS Modules вместо обычного CSS для изоляции стилей.
2. Название компонента
myComponent→UserList— компоненты всегда называются с большой буквы (PascalCase), иначе React не распознает их как компоненты.
3. Деструктуризация пропсов
(props)→{ title, subtitle, onItemClick }— явная деструктуризация с типами.onItemClickвынесен из внутренней функции в пропс — компонент становится переиспользуемым.
4. Семантические теги
div.container→sectiondiv.header→headerdiv.title→h2div.subtitle→pdiv.list→uldiv.item→lidiv.item-date→timeс атрибутомdateTimeinput type="text"→input type="search"сaria-label
5. Оптимизация вычислений
filteredItemsобёрнут вuseMemo— фильтрация не пересчитывается при каждом рендере.- Функция
filterItemsвынесена за пределы компонента — не создаётся заново при каждом рендере.
6. Оптимизация обработчиков
handleFilterChangeобёрнут вuseCallback— стабильная ссылка.- Вместо отдельного
onClickдля каждогоli— делегирование событий: один обработчик наulсdata-item-idатрибутами. Это уменьшает количество слушателей с N до 1.
7. Обработка состояний
- Добавлен
isMountedфлаг для предотвращения обновления состояния размонтированного компонента. - Ранние возвраты для
loadingиerror— читаемость. role="alert"для сообщения об ошибке — accessibility.
8. Дополнительные улучшения
- Типизация через TypeScript интерфейс.
- CSS Modules вместо глобальных классов.
tabIndex={0}иrole="button"наli— доступность с клавиатуры.- Подсчёт отфильтрованных элементов в футере.
Вопрос 48. В чём главное отличие MobX от других стейт-менеджеров (Redux, Zustand)?
Таймкод: 01:09:31
Ответ собеседника: неполный. MobX отличается реактивностью, основанной на прокси (Proxy) объектами, что позволяет мутировать данные напрямую и автоматически отслеживать изменения. Состояние всегда остаётся консистентным без необходимости писать boilerplate-код, как в Redux (редюсеры, экшены, combineReducers). MobX — один из первых стейт-менеджеров в экосистеме React, пришёл из Angular. Для работы с промисами нужен дополнительный пакет MobX-utils.
Правильный ответ:
MobX принципиально отличается от Redux и Zustand подходом к управлению состоянием — он использует автоматическую реактивность на основе прокси, а не явные обновления через dispatch.
Ключевое отличие: императивная мутация vs декларативные обновления
Redux/Zustand — вы явно описываете, как состояние должно измениться:
// Redux: action + reducer
dispatch({ type: 'ADD_TODO', payload: 'Buy milk' });
// Zustand: setState
set(state => ({ todos: [...state.todos, 'Buy milk'] }));
MobX — вы просто мутируете данные, а MobX автоматически отслеживает изменения:
// MobX: прямая мутация
store.todos.push('Buy milk'); // React компоненты обновятся автоматически
Как работает MobX под капотом:
MobX использует ES6 Proxy для оборачивания объектов состояния. При чтении свойства MobX отслеживает, какой компонент (observer) его использует. При записи — уведомляет все зависимые компоненты:
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this); // Всё становится реактивным
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false }); // Просто мутируем
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed; // Просто мутируем
}
setFilter(filter) {
this.filter = filter;
}
// Вычисляемые значения — автоматически кэшируются
get filteredTodos() {
switch (this.filter) {
case 'active': return this.todos.filter(t => !t.completed);
case 'completed': return this.todos.filter(t => t.completed);
default: return this.todos;
}
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
}
export const todoStore = new TodoStore();
// Компонент автоматически перерисовывается при изменении зависимых свойств
import { observer } from 'mobx-react-lite';
const TodoList = observer(() => {
return (
<ul>
{todoStore.filteredTodos.map(todo => (
<li key={todo.id} onClick={() => todoStore.toggleTodo(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
);
});
Сравнительная таблица:
| Характеристика | MobX | Redux | Zustand |
|---|---|---|---|
| Обновление состояния | Прямая мутация | dispatch + reducer | setState |
| Boilerplate | Минимальный | Большой | Минимальный |
| Реактивность | Автоматическая (Proxy) | Ручная (subscribe) | Ручная (subscribe) |
| Вычисляемые значения | get с автокэшированием | reselect | Вручную или через производные |
| DevTools | mobx-devtools | Redux DevTools (отличные) | Redux DevTools |
| Кривая обучения | Средняя | Высокая | Низкая |
| TypeScript | Отличная поддержка | Отличная | Отличная |
| Размер бандла | ~16KB | ~1KB (+ middleware) | ~1KB |
Когда выбирать MobX:
- Сложные связанные данные с множеством вычисляемых значений (фильтры, сортировки, агрегации).
- Когда хочется мутировать данные напрямую без boilerplate.
- Большие формы с зависимыми полями.
- Когда команда предпочитает ООП-подход.
Когда выбрать Zustand/Redux:
- Нужна предсказуемость и воспроизводимость обновлений (Redux).
- Простой стор без сложной логики.
- Нужны middleware (логирование, персистентность, синхронизация).
- Нужна интеграция с Redux DevTools.
Потенциальные проблемы MobX:
- Магическая реактивность может быть неочевидной — сложнее отследить, где и почему обновился компонент.
- Без
observer()обёртки компонент не будет реагировать на изменения. - Прямая мутация может привести к неожиданным побочным эффектам при отсутствии дисциплины.
- Меньше сообщество и экосистема по сравнению с Redux.
Вопрос 48. В чём главное отличие MobX от других стейт-менеджеров (Redux, Zustand)?
Таймкод: 01:09:31
Ответ собеседника: неполный. MobX отличается реактивностью, основанной на прокси (Proxy) объектами, что позволяет мутировать данные напрямую и автоматически отслеживать изменения. Состояние всегда остаётся консистентным без необходимости писать boilerplate-код, как в Redux (редюсеры, экшены, combineReducers). MobX — один из первых стейт-менеджеров в экосистеме React, пришёл из Angular. Для работы с промисами нужен дополнительный пакет MobX-utils.
Правильный ответ:
MobX принципиально отличается от Redux и Zustand подходом к управлению состоянием — он использует автоматическую реактивность на основе прокси, а не явные обновления через dispatch.
Ключевое отличие: императивная мутация vs декларативные обновления
Redux/Zustand — вы явно описываете, как состояние должно измениться:
// Redux: action + reducer
dispatch({ type: 'ADD_TODO', payload: 'Buy milk' });
// Zustand: setState
set(state => ({ todos: [...state.todos, 'Buy milk'] }));
MobX — вы просто мутируете данные, а MobX автоматически отслеживает изменения:
// MobX: прямая мутация
store.todos.push('Buy milk'); // React компоненты обновятся автоматически
Как работает MobX под капотом:
MobX использует ES6 Proxy для оборачивания объектов состояния. При чтении свойства MobX отслеживает, какой компонент (observer) его использует. При записи — уведомляет все зависимые компоненты:
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this); // Всё становится реактивным
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false }); // Просто мутируем
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed; // Просто мутируем
}
setFilter(filter) {
this.filter = filter;
}
// Вычисляемые значения — автоматически кэшируются
get filteredTodos() {
switch (this.filter) {
case 'active': return this.todos.filter(t => !t.completed);
case 'completed': return this.todos.filter(t => t.completed);
default: return this.todos;
}
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
}
export const todoStore = new TodoStore();
// Компонент автоматически перерисовывается при изменении зависимых свойств
import { observer } from 'mobx-react-lite';
const TodoList = observer(() => {
return (
<ul>
{todoStore.filteredTodos.map(todo => (
<li key={todo.id} onClick={() => todoStore.toggleTodo(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
);
});
Сравнительная таблица:
| Характеристика | MobX | Redux | Zustand |
|---|---|---|---|
| Обновление состояния | Прямая мутация | dispatch + reducer | setState |
| Boilerplate | Минимальный | Большой | Минимальный |
| Реактивность | Автоматическая (Proxy) | Ручная (subscribe) | Ручная (subscribe) |
| Вычисляемые значения | get с автокэшированием | reselect | Вручную или через производные |
| DevTools | mobx-devtools | Redux DevTools (отличные) | Redux DevTools |
| Кривая обучения | Средняя | Высокая | Низкая |
| TypeScript | Отличная поддержка | Отличная | Отличная |
| Размер бандла | ~16KB | ~1KB (+ middleware) | ~1KB |
Когда выбирать MobX:
- Сложные связанные данные с множеством вычисляемых значений (фильтры, сортировки, агрегации).
- Когда хочется мутировать данные напрямую без boilerplate.
- Большие формы с зависимыми полями.
- Когда команда предпочитает ООП-подход.
Когда выбрать Zustand/Redux:
- Нужна предсказуемость и воспроизводимость обновлений (Redux).
- Простой стор без сложной логики.
- Нужны middleware (логирование, персистентность, синхронизация).
- Нужна интеграция с Redux DevTools.
Потенциальные проблемы MobX:
- Магическая реактивность может быть неочевидной — сложнее отследить, где и почему обновился компонент.
- Без
observer()обёртки компонент не будет реагировать на изменения. - Прямая мутация может привести к неожиданным побочным эффектам при отсутствии дисциплины.
- Меньше сообщество и экосистема по сравнению с Redux.
Вопрос 49. Какой стек технологий используется на проекте и планируется ли использование стейт-менеджера?
Таймкод: 01:19:13
Ответ собеседника: неполный. Используется React 19, React Query, роутинг и SAAS. Стейт-менеджер пока не особо нужен, но если понадобится, планируется использовать что-то легковесное, например Zustand.
Правильный ответ:
Отличный ответ — кандидат демонстрирует зрелый подход к выбору стека и понимание, когда стейт-менеджер действительно необходим. Дополню контекстом.
Текущий стек и его обоснование:
React 19 — актуальная версия с новыми возможностями:
- Server Components — рендеринг на сервере без отправки JS клиенту.
use()хук — чтение ресурсов (промисов, контекста) прямо в рендере.- Actions — асинхронные операции с автоматическим управлением состоянием загрузки.
useOptimistic— оптимистичные обновления без лишнего кода.
React Query (TanStack Query) — выполняет роль асинхронного стейт-менеджера:
// React Query управляет: кэшированием, фоновыми обновлениями, stale-while-revalidate
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 минут данные считаются свежими
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <Profile user={data} />;
}
React Query покрывает значительную часть задач, которые обычно решает стейт-менеджер:
- Кэширование данных.
- Фоновые обновления.
- Оптимистичные обновления.
- Инвалидация кэша.
Когда Zustand (или другой стейт-менеджер) действительно нужен:
Даже с React Query остаются сценарии, требующие клиентского стейт-менеджера:
1. Комплексное клиентское состояние:
// Состояние UI: модальные окна, сайдбары, темы, фильтры
const useUIStore = create((set) => ({
sidebarOpen: false,
theme: 'light',
activeModal: null,
filters: { status: 'all', sortBy: 'date' },
toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
setTheme: (theme) => set({ theme }),
setFilter: (key, value) => set(state => ({
filters: { ...state.filters, [key]: value }
})),
}));
2. Состояние, разделяемое между разными доменами:
// Корзина покупок — нужна и в хедере (счётчик), и на странице корзины, и в чекауте
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set(state => {
const existing = state.items.find(i => i.id === product.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id)
})),
get totalItems() {
return get().items.reduce((sum, i) => sum + i.quantity, 0);
},
get totalPrice() {
return get().items.reduce((sum, i) => sum + i.price * i.quantity, 0);
},
}));
3. Состояние с зависимостями от серверных данных:
// React Query получает данные, Zustand управляет их представлением
function ProjectBoard() {
const { data: tasks } = useQuery({
queryKey: ['tasks'],
queryFn: fetchTasks
});
const { groupBy, setGroupBy } = useBoardStore();
// Группировка и сортировка — клиентская логика
const groupedTasks = useMemo(() => {
if (!tasks) return {};
return groupTasks(tasks, groupBy);
}, [tasks, groupBy]);
return <Board groupedTasks={groupBy} onGroupByChange={setGroupBy} />;
}
Почему Zustand — хороший выбор:
| Критерий | Zustand | Redux | MobX |
|---|---|---|---|
| Размер | ~1KB | ~1KB + middleware | ~16KB |
| Boilerplate | Минимальный | Значительный | Минимальный |
| TypeScript | Отличный | Отличный | Отличный |
| DevTools | Через middleware | Отличные | mobx-devtools |
| Middleware | Есть (persist, immer) | Богатая экосистема | Есть |
| Кривая обучения | Низкая | Высокая | Средняя |
Альтернатива — встроенные средства React:
Для простых случаев можно обойтись без стейт-менеджера:
// Context + useReducer для локального состояния
const CartContext = createContext(null);
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
const value = useMemo(() => ({
...state,
addItem: (item) => dispatch({ type: 'ADD_ITEM', payload: item }),
removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', payload: id }),
}), [state]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
Вывод: подход «React Query для серверного состояния + Zustand для клиентского» — это современный, сбалансированный стек, который покрывает большинство сценариев без избыточной сложности.
Вопрос 49. Пишутся ли тесты на проекте?
Таймкод: 01:19:31
Ответ собеседника: правильный. Контрагенты пытались писать тесты, но не хватало времени, поэтому тестирование попало в техдолг.
Правильный ответ:
Это честный и распространённый ответ. Ситуация, когда тесты попадают в техдолг из-за дедлайнов — типичная проблема в индустрии. Разберём, как грамотно выстраивать тестирование и как выбраться из этой ситуации.
Типичная пирамида тестирования:
/ E2E \ ← Мало (5-10%), дорогие, медленные
/--------\
/ Integration \ ← Средне (20-30%), проверяют взаимодействие
/--------------\
/ Unit Tests \ ← Много (60-70%), быстрые, дешёвые
/------------------\
Как приоритизировать тесты при нехватке времени:
1. Начинать с критического пути (happy path):
// Тест критического пути — минимальная польза при минимальных затратах
describe('Checkout flow', () => {
it('should complete purchase with valid card', async () => {
render(<Checkout cart={mockCart} />);
await userEvent.type(screen.getByLabelText(/card number/), '4242424242424242');
await userEvent.type(screen.getByLabelText(/expiry/), '12/25');
await userEvent.type(screen.getByLabelText(/cvv/), '123');
await userEvent.click(screen.getByRole('button', { name: /pay/i }));
expect(await screen.findByText(/payment successful/i)).toBeInTheDocument();
});
});
2. Покрывать бизнес-логику, а не реализацию:
// Плохо — тестирует реализацию (хрупкий тест)
test('should call setState with correct value', () => {
const setState = jest.fn();
const hook = renderHook(() => useCounter(setState));
hook.result.current.increment();
expect(setState).toHaveBeenCalledWith(1); // Сломается при рефакторинге
});
// Хорошо — тестирует поведение (устойчивый тест)
test('should display incremented count', () => {
render(<Counter />);
fireEvent.click(screen.getByText('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
3. Использовать стратегию «тестирование при багах»:
// Каждый найденный баг → сначала тест, потом фикс
// Баг: при пустом поисковом запросе приложение падает
describe('SearchInput', () => {
it('should not crash on empty search', () => {
render(<SearchInput onSearch={mockOnSearch} />);
fireEvent.change(screen.getByRole('searchbox'), { target: { value: '' } });
fireEvent.submit(screen.getByRole('searchbox'));
expect(mockOnSearch).toHaveBeenCalledWith('');
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
Практический минимум для проекта без тестов:
1. Unit-тесты для утилит и чистых функций:
// utils/calculations.ts
export function calculateDiscount(price: number, discountPercent: number): number {
if (price < 0) throw new Error('Price must be positive');
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Discount must be between 0 and 100');
}
return price * (1 - discountPercent / 100);
}
// utils/calculations.test.ts
describe('calculateDiscount', () => {
it('should apply discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('should throw on negative price', () => {
expect(() => calculateDiscount(-10, 20)).toThrow('Price must be positive');
});
it('should throw on invalid discount', () => {
expect(() => calculateDiscount(100, 150)).toThrow('Discount must be between 0 and 100');
});
});
2. Компонентные тесты для ключевых UI-элементов:
// components/Button.test.tsx
describe('Button', () => {
it('should render with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('should call onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('should be accessible via keyboard', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByRole('button').focus();
fireEvent.keyDown(document.activeElement!, { key: 'Enter' });
expect(handleClick).toHaveBeenCalled();
});
});
3. Интеграционные тесты для ключевых пользовательских сценариев:
// features/auth/LoginFlow.test.tsx
describe('Login flow', () => {
it('should log in with valid credentials', async () => {
render(<LoginPage />);
await userEvent.type(screen.getByLabelText(/email/), 'user@example.com');
await userEvent.type(screen.getByLabelText(/password/), 'password123');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/welcome back/i)).toBeInTheDocument();
});
it('should show error on invalid credentials', async () => {
server.use(
rest.post('/api/login', (req, res, ctx) => {
return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
})
);
render(<LoginPage />);
await userEvent.type(screen.getByLabelText(/email/), 'wrong@example.com');
await userEvent.type(screen.getByLabelText(/password/), 'wrongpass');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
});
});
Как внедрить тесты при наличии техдолга:
Стратегия «Boy Scout Rule» — оставляй код лучше, чем нашёл:
// Перед изменением существующего кода — напиши тесты
// 1. Понять текущее поведение
// 2. Написать тесты, фиксирующие это поведение
// 3. Рефакторить с уверенностью, что ничего не сломал
describe('LegacyPaymentProcessor', () => {
it('should process payment with current behavior', () => {
const processor = new LegacyPaymentProcessor();
const result = processor.process({ amount: 100, currency: 'USD' });
expect(result).toEqual({
id: expect.any(String),
status: 'success',
amount: 100,
});
});
});
Рекомендуемый стек для тестирования в React-проекте:
| Инструмент | Назначение |
|---|---|
| Jest | Тестовый раннер, моки, снапшоты |
| React Testing Library | Тестирование компонентов |
| MSW (Mock Service Worker) | Мокирование API |
| Playwright / Cypress | E2E тесты |
| Vitest | Альтернатива Jest (быстрее, нативный ESM) |
Метрики покрытия — не цель, а индикатор:
// jest.config.js — разумные пороги
{
"coverageThresholds": {
"global": {
"branches": 60,
"functions": 70,
"lines": 70,
"statements": 70
},
// Критические модули — выше
"./src/features/payment/**": {
"branches": 80,
"functions": 90,
"lines": 90,
"statements": 90
}
}
}
Вывод: кандидат честно признаёт проблему с тестами. Это нормально — важно понимать, как двигаться дальше: начинать с критического пути, писать тесты при каждом баге, постепенно повышать покрытие. Главное — не стремиться к 100% покрытию, а тестировать то, что действительно важно для бизнеса.
Вопрос 50. Какие задачи будут ставиться на испытательный срок?
Таймкод: 01:19:54
Ответ собеседника: правильный. Задачи связаны с админпанелью, частью SAAS и переносом некоторых страниц на новый дизайн для медицинских организаций. Также идёт активная интеграция с бэкендом, который переписывается на Go.
Правильный ответ:
Отличный ответ — кандидат получил конкретную информацию о задачах и понимает контекст проекта. Разберём подробнее, что из себя представляет типичный испытательный срок для frontend-разработчика и как к нему подготовиться.
Типичные задачи на испытательный срок:
1. Онбординг и первые задачи (1-2 неделя):
- Настройка окружения (IDE, Git, CI/CD).
- Изучение кодовой базы и архитектуры проекта.
- Первые мелкие задачи: исправление багов, небольшие UI-правки.
- Знакомство с командой и процессами.
// Пример типовой первой задачи — исправление бага
// Баг: кнопка "Сохранить" не блокируется при отправке формы
function SaveButton({ loading, onClick }) {
return (
<button
type="submit"
disabled={loading}
aria-busy={loading}
onClick={onClick}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
);
}
2. Средние задачи (2-4 неделя):
- Реализация новых компонентов по дизайн-макетам.
- Интеграция с API.
- Написание тестов для своего кода.
// Типовая задача — реализация компонента админпанели
function OrganizationCard({ organization, onEdit, onDelete }) {
return (
<Card>
<CardHeader>
<h3>{organization.name}</h3>
<Badge variant={organization.status === 'active' ? 'success' : 'warning'}>
{organization.status}
</Badge>
</CardHeader>
<CardBody>
<p>{organization.address}</p>
<p>ИНН: {organization.inn}</p>
</CardBody>
<CardFooter>
<Button variant="secondary" onClick={() => onEdit(organization.id)}>
Редактировать
</Button>
<Button variant="danger" onClick={() => onDelete(organization.id)}>
Удалить
</Button>
</CardFooter>
</Card>
);
}
3. Более сложные задачи (1-3 месяц):
- Перенос существующих страниц на новый дизайн.
- Реализация сложных форм с валидацией.
- Работа с интеграцией бэкенд-фронтенд.
// Пример: форма для медицинской организации
function MedicalOrgForm({ initialData, onSubmit }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
defaultValues: initialData,
resolver: zodResolver(medicalOrgSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
label="Название организации"
error={errors.name?.message}
{...register('name')}
required
/>
<FormField
label="ИНН"
error={errors.inn?.message}
{...register('inn')}
pattern="\d{10}|\d{12}"
required
/>
<FormField
label="Адрес"
error={errors.address?.message}
{...register('address')}
required
/>
<FormField
label="Лицензия"
error={errors.license?.message}
{...register('license')}
/>
<Button type="submit" loading={isSubmitting}>
{initialData ? 'Сохранить' : 'Создать'}
</Button>
</form>
);
}
Как успешно пройти испытательный срок:
1. Задавайте вопросы:
- Не стесняйтесь уточнять требования.
- Уточняйте критерии приёмки задачи.
- Просите code review раньше, а не в последний момент.
2. Коммуницируйте:
- Докладывайте о прогрессе и блокерах.
- Если задача занимает больше времени — сообщите заранее.
- Показывайте промежуточные результаты.
3. Следуйте стандартам проекта:
- Используйте тот же стиль кода, что и в проекте.
- Следуйте принятым соглашениям по именованию.
- Не меняйте архитектуру без обсуждения.
4. Показывайте инициативу:
- Предлагайте улучшения, но не навязывайте их.
- Документируйте неочевидные решения.
- Помогайте коллегам, когда есть время.
Типичные критерии оценки на испытательном сроке:
| Критерий | Что оценивается |
|---|---|
| Качество кода | Читаемость, соответствие стандартам |
| Скорость | Выполнение задач в срок |
| Самостоятельность | Умение находить решения |
| Коммуникация | Прозрачность, своевременность обратной связи |
| Командная работа | Взаимодействие с коллегами |
| Обучаемость | Скорость погружения в новый стек |
Вывод: кандидат получил конкретную информацию о задачах — это хороший знак. Задачи на испытательный срок обычно подбираются так, чтобы оценить ключевые компетенции: технические навыки, самостоятельность, коммуникацию и способность работать в команде.
Вопрос 50. Какие задачи будут ставиться на испытательный срок?
Таймкод: 01:19:54
Ответ собеседника: правильный. Задачи связаны с админпанелью, частью SAAS и переносом некоторых страниц на новый дизайн для медицинских организаций. Также идёт активная интеграция с бэкендом, который переписывается на Go.
Правильный ответ:
Хороший ответ — кандидат получил конкретную информацию о задачах и понимает контекст проекта. Дополню типичными рекомендациями по прохождению испытательного срока.
Типичные задачи на испытательный срок для frontend-разработчика:
Первые 1-2 недели — онбординг:
- Настройка окружения, изучение кодовой базы.
- Мелкие багфиксы, небольшие UI-правки.
- Знакомство с командой и процессами.
2-4 неделя — средние задачи:
- Реализация новых компонентов по дизайн-макетам.
- Интеграция с API (в данном случае — с Go-бэкендом).
- Написание тестов для своего кода.
1-3 месяц — более сложные задачи:
- Перенос существующих страниц на новый дизайн.
- Реализация сложных форм с валидацией (админпанель медицинских организаций).
- Самостоятельная работа над фичей от дизайна до деплоя.
Рекомендации для успешного прохождения:
- Задавайте вопросы и уточняйте требования — лучше спросить лишний раз, чем переделывать.
- Докладывайте о прогрессе и блокерах — прозрацность важнее иллюзии продуктивности.
- Следуйте стандартам проекта — стиль кода, именование, структура.
- Показывайте инициативу, но не ломайте существующую архитектуру без обсуждения.
Вывод: кандидат демонстрирует реалистичное понимание задач испытательного срока. Это хороший знак — он знает, что его ждёт, и готов к работе с админпанелью, интеграцией с Go-бэкендом и переносом дизайна.
Вопрос 51. Есть ли переработки в команде?
Таймкод: 01:21:22
Ответ собеседника: правильный. Переработок нет. Сейчас основная деятельность — найм инженеров и работа с контрагентами. Планируется собрать команду, которая будет дополнять качество своей работы. Процессы выстраиваются по Scrum с лучшими ритуалами. Первые 1-3 месяца — притирка по сторипоинтам и понимание возможностей команды.
Правильный ответ:
Отличный и честный ответ. Кандидат получил реалистичную картину о рабочих условиях. Разберём подробнее, что это значит и на что обратить внимание.
Текущая ситуация в команде:
1. Фаза формирования команды:
- Активный найм инженеров — значит, команда растёт.
- Работа с контрагентами — часть работы выполняется внешними подрядчиками.
- Переход к продуктовой разработке — это нормальный этап зрелости компании.
2. Процессы по Scrum:
Типичный Scrum-цикл (спринт 2 недели):
┌─────────────────────────────────────────────────────────────┐
│ Понедельник: Планирование спринта (2-4 часа) │
│ Ежедневно: Daily standup (15 мин) │
│ Среда/четверг: Уточнение бэклога (1 час) │
│ Пятница: Демо + Ретроспектива (1-2 часа) │
└─────────────────────────────────────────────────────────────┘
3. Период притирки (1-3 месяца):
- Калибровка сторипоинтов — команда учится оценивать задачи реалистично.
- Понимание возможностей — кто в чём силён, как распределять нагрузку.
- Выстраивание коммуникации — как общаться, как эскалировать, как давать фидбек.
На что стоит обратить внимание кандидату:
Положительные сигналы:
- Отсутствие переработок — здоровая культура.
- Процессы выстраиваются — есть структура, а не хаос.
- Планируют собирать команду — инвестиции в развитие.
- Scrum с ритуалами — значит, есть дисциплина.
Вопросы, которые стоит уточнить:
- Какой размер команды планируется?
- Как распределяется работа между контрагентами и внутренней командой?
- Какой технологический стек используется?
- Как часто проводятся ретроспективы и что на них обсуждается?
Вывод: кандидат получил честный ответ о текущем состоянии команды. Это позитивно — компания на этапе роста, процессы выстраиваются, переработок нет. Для Go-разработчика это хороший контекст — можно влиять на формирование команды и процессов с самого начала.
