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

Собеседование на Junior Frontend разработчика на React

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

Сегодня мы разберём собеседование кандидата на позицию junior frontend-разработчика, в ходе которого Юля продемонстрировала уверенное владение основами HTML, CSS, TypeScript и React — включая семантическую вёрстку, псевдо-классы и псевдо-элементы, различия между типами и интерфейсами, работу с хуками, виртуальным DOM и серверным рендерингом. Несмотря на небольшие затруднения с более сложными темами (например, компоненты высшего порядка), её ответы показали глубокое понимание ключевых концепций, стремление к обучению и готовность к росту до уровня middle. Интервьюер отметил, что уровень знаний Юли превышает ожидания для junior-позиции, и с уверенностью рекомендовал её к найму.

Вопрос 1. Расскажи о себе, кем работаешь и куда хочешь развиваться.

Таймкод: 00:00:36

Ответ собеседника: Правильный. Младший разработчик в компании, работаю над стартапом с виртуальными машинами, проект на TypeScript, верстаю HTML/CSS. В свободное время учусь, хочу попробовать себя в React, рассматриваю фреймворки для серверного рендеринга.

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

Это вводный вопрос, который позволяет интервьюеру оценить ваш общий уровень, мотивацию и карьерные цели. Кандидат дал честный и структурированный ответ, что является хорошим подходом.

Рекомендуемая структура ответа для позиции Golang-разработчика:

Текущая роль и опыт

Стоит кратко описать текущую должность, технологический стек и зону ответственности. Например: «Работаю разработчиком в компании X, занимаюсь бэкенд-разработкой микросервисов на Go, участвую в проектировании API, интеграции с базами данных и оптимизации производительности».

Ключевые достижения

Важно упомянуть конкретные результаты: снижение латентности, масштабирование системы, внедрение новых технологий. Например: «Оптимизировал запросы к PostgreSQL, что сократило время ответа API на 40%» или «Мигрировал монолитный сервис на микросервисную архитектуру, улучшив отказоустойчивость».

Направление развития

Для Golang-позиции стоит показать интерес к углублению в язык и экосистему: системное программирование, конкурентность, работа с низкоуровневыми API, участие в open-source проектах на Go, изучение внутренних механизмов runtime (планировщик, garbage collector, модель памяти).

Что стоит добавить в ответ для Golang-вакансии:

  • Опыт работы с Go: версии языка, используемые фреймворки (gin, echo, fiber, chi), библиотеки (gorm, sqlx, zap, viper).
  • Понимание конкурентности в Go: горутины, каналы, пакеты sync, context.
  • Опыт работы с базами данных: PostgreSQL, MySQL, Redis, MongoDB.
  • Знание инфраструктурных инструментов: Docker, Kubernetes, CI/CD, мониторинг (Prometheus, Grafana).
  • Понимание принципов проектирования: SOLID, Clean Architecture, Domain-Driven Design.

Пример улучшенного ответа:

«Работаю Go-разработчиком около двух лет. Занимаюсь разработкой и поддержкой микросервисной архитектуры высоконагруженной платежной системы. В повседневной работе использую Go 1.21+, фреймворк chi для REST API, PostgreSQL в качестве основной базы данных, Redis для кэширования, Kafka для асинхронной обработки событий. Активно применяю горутины и каналы для параллельной обработки транзакций, использую context для управления временем жизни запросов. В свободное время изучаю внутренности Go runtime, участвую в open-source проектах. Хочу развиваться в сторону системного проектирования и архитектуры распределенных систем».

Вопрос 2. Чем отличается JSX от HTML и что у них общего.

Таймкод: 00:02:30

Ответ собеседника: Правильный. JSX похож на HTML, позволяет создавать компоненты через createElement, но так как это неудобно и нечитаемо, разработчики придумали JSX как синтаксис, который в результате всё равно превращается в HTML-элементы.

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

Кандидат верно уловил суть различий, но ответ можно дополнить техническими деталями. Это вопрос по фронтенд-тематике, и хотя вакансия на Golang-разработчика, понимание смежных технологий показывает широту кругозора.

Что общего между JSX и HTML

  • Оба используют похожий декларативный синтаксис для описания структуры пользовательского интерфейса.
  • Оба работают с деревом элементов (DOM-деревом).
  • Оба поддерживают вложенность элементов, атрибуты и текстовое содержимое.
  • Концептуально JSX был создан для того, чтобы разработчики, привыкшие к HTML, могли быстро освоить создание интерфейсов в React.

Ключевые отличия JSX от HTML

JSX — это JavaScript-расширение, а не шаблонный язык. JSX не является HTML-кодом. Это синтаксический сахар для вызова React.createElement(component, props, ...children). После компиляции (через Babel или встроенный компилятор React) JSX превращается в обычные JavaScript-вызовы, которые возвращают объекты, описывающие виртуальный DOM.

Использование выражений JavaScript. Внутри JSX можно использовать любые JavaScript-выражения с помощью фигурных скобок {}:

const name = "World";
const element = <h1>Hello, {name}!</h1>;

const items = [1, 2, 3];
const list = (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);

В HTML такой возможности нет.

Атрибуты переименованы в camelCase. В JSX используются имена свойств DOM-объектов, а не HTML-атрибуты:

// JSX
<div className="container" tabIndex={0} onClick={handleClick}>Content</div>

// HTML
<div class="container" tabindex="0">Content</div>

Здесь class стал className, потому что class — зарезервированное слово в JavaScript. Аналогично tabindex стал tabIndex, onclick стал onClick.

Закрытие пустых тегов. В JSX все теги должны быть закрыты:

// JSX — обязательно закрывать
<img src="image.png" />
<input type="text" />
<br />

// HTML — допустимо не закрывать
<img src="image.png">
<input type="text">
<br>

style — это объект, а не строка. В JSX атрибут style принимает JavaScript-объект:

// JSX
<div style={{ color: 'red', fontSize: '14px' }}>Text</div>

// HTML
<div style="color: red; font-size: 14px;">Text</div>

Условный рендеринг и циклы. JSX позволяет использовать JavaScript для логики отображения:

// Условный рендеринг
{isLoggedIn ? <Dashboard /> : <Login />}

// Циклы
{users.map(user => <UserCard key={user.id} user={user} />)}

В HTML для этого нужны отдельные шаблонные движки или JavaScript.

Фрагменты. JSX поддерживает фрагменты для возврата нескольких элементов без обертки:

return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);

Фундаментальное отличие. HTML — это статическая разметка, которую браузер парсит и отображает. JSX — это динамический декларативный способ описания интерфейса, который после компиляции становится JavaScript-кодом, управляющим виртуальным DOM. JSX позволяет смешивать разметку с логикой, что делает компоненты самодостаточными и переиспользуемыми.

Вопрос 3. Что такое семантическая вёрстка и приведи примеры семантических тегов.

Таймкод: 00:03:26

Ответ собеседника: Правильный. Семантическая вёрстка повышает доступность сайтов. Примеры тегов: header, nav, section, article, footer. Поисковые роботы смотрят на структуру — где статья, где заголовок.

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

Кандидат дал корректный ответ, затронув ключевые аспекты. Для полноты картины стоит раскрыть тему глубже.

Определение семантической вёрстки

Семантическая вёрстка — это подход к созданию HTML-разметки, при котором теги выбираются исходя из смыслового значения содержимого, а не из внешнего вида. Каждый элемент должен нести информацию о роли своего содержимого в структуре документа.

Зачем нужна семантическая вёрстка

Доступность (Accessibility). Экранные читалки (screen readers) используют семантические теги для навигации по странице. Например, пользователь screen reader может перейти к навигации по тегу <nav>, к основному контенту по <main>, или получить список всех статей на странице по тегам <article>.

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

Поддерживаемость кода. Семантическая разметка делает код более читаемым и понятным для разработчиков. Вместо множества <div class="header"> сразу видно назначение каждого блока.

Основные семантические теги и их назначение

<header> — вводная часть страницы или секции. Обычно содержит логотип, заголовок, навигацию.

<nav> — блок навигационных ссылок. Не все группы ссылок должны быть в <nav> — только основные навигационные блоки.

<main> — основное содержимое страницы. На странице должен быть только один элемент <main>, и он не должен быть вложен в <article>, <aside>, <footer>, <header> или <nav>.

<section> — тематическая группа контента, обычно с заголовком. Используется для логического разделения содержимого.

<article> — самодостаточная независимая часть контента, которая имеет смысл сама по себе: статья в блоге, комментарий, виджет, новость.

<aside> — дополнительный контент, косвенно связанный с основным: боковые панели, рекламные блоки, цитаты.

<footer> — завершающая часть страницы или секции. Содержит информацию об авторе, копирайт, контакты.

<figure> и <figcaption> — иллюстрация, диаграмма, код с подписью.

<time> — дата или время в машиночитаемом формате.

<address> — контактная информация автора или владельца документа.

<details> и <summary> — раскрывающийся блок без JavaScript.

Пример структуры страницы с семантической разметкой:


<body>
<header>
<h1>Название сайта</h1>
<nav>
<ul>
<li><a href="/">Главная</a></li>
<li><a href="/about">О нас</a></li>
</ul>
</nav>
</header>

<main>
<article>
<header>
<h2>Заголовок статьи</h2>
<time datetime="2024-01-15">15 января 2024</time>
</header>
<section>
<h3>Введение</h3>
<p>Текст введения...</p>
</section>
<section>
<h3>Основная часть</h3>
<p>Основной контент...</p>
<figure>
<img src="diagram.png" alt="Диаграмма">
<figcaption>Рисунок 1: Схема работы</figcaption>
</figure>
</section>
</article>

<aside>
<h3>Похожие статьи</h3>
<ul>
<li><a href="/article-2">Статья 2</a></li>
</ul>
</aside>
</main>

<footer>
<p>&copy; 2024 Название компании</p>
<address>contact@example.com</address>
</footer>
</body>

Антипаттерн — div-супермаркет:

<!-- Плохо: непонятно назначение блоков -->
<div class="header">
<div class="nav">...</div>
</div>
<div class="content">
<div class="article">...</div>
</div>
<div class="footer">...</div>

<!-- Хорошо: семантическая структура -->
<header>
<nav>...</nav>
</header>
<main>
<article>...</article>
</main>
<footer>...</footer>

Семантическая вёрстка — это не просто замена <div> на именованные теги. Это осознанный выбор тега в соответствии со смыслом содержимого, что делает веб более доступным, понятным и структурированным.

Вопрос 4. На что влияет доступность (accessibility) и что такое screen reader.

Таймкод: 00:04:21

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

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

Кандидат верно описал назначение screen reader, но ответ можно существенно дополнить, так как тема доступности обширная и важная.

Что такое screen reader (экранная читалка)

Screen reader — это программное обеспечение, которое преобразует визуальное содержимое экрана в речь или тактильный вывод (шрифт Брайля). Оно позволяет людям с нарушениями зрения взаимодействовать с компьютером и веб-сайтами.

Популярные screen readers:

  • JAWS (Job Access With Speech) — коммерческий, самый распространённый на Windows.
  • NVDA (NonVisual Desktop Access) — бесплатный открытый для Windows.
  • VoiceOver — встроенный в macOS и iOS.
  • TalkBack — встроенный в Android.
  • Orca — для Linux.

Как работает screen reader

Screen reader читает не визуальную страницу, а её семантическую структуру — accessibility tree, которая строится на основе DOM и ARIA-атрибутов. Пользователь навигируется по странице с помощью клавиатуры: переходит между заголовками, ссылками, формами, кнопками. Screen reader озвучивает роль, имя и состояние каждого элемента.

На что влияет доступность (a11y)

Социальный аспект. По данным ВОЗ, около 2,2 миллиарда человек в мире имеют нарушения зрения. Доступность веб-сайтов позволяет этим людям полноценно пользоваться цифровыми сервисами: совершать покупки, получать образование, работать.

Юридический аспект. Во многих странах существуют законы, обязывающие обеспечивать доступность цифровых продуктов. В США это ADA (Americans with Disabilities Act) и Section 508, в Европе — European Accessibility Act. Несоблюдение может привести к судебным искам и штрафам.

Бизнес-аспект. Доступность расширяет аудиторию продукта. Кроме того, многие практики доступности улучшают пользовательский опыт для всех: понятная структура страницы, навигация с клавиатуры, контрастные цвета полезны не только для людей с инвалидностью.

SEO. Поисковые системы и screen reader используют одну и ту же семантическую структуру страницы. Правильная доступность улучшает индексацию.

Ключевые принципы доступности (WCAG)

WCAG (Web Content Accessibility Guidelines) определяет четыре принципа, известные как POUR:

Perceivable (Воспринимаемый). Контент должен быть представлен в формах, которые пользователь может воспринимать: текстовые альтернативы для изображений, субтитры для видео, достаточная контрастность текста.

Operable (Управляемый). Интерфейс должен быть управляемым: вся функциональность доступна с клавиатуры, пользователь имеет достаточно времени для чтения, контент не вызывает судорог (нет мигания с частотой более 3 раз в секунду).

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

Robust (Устойчивый). Контент должен быть достаточно надёжным для работы с различными технологиями, включая вспомогательные: правильная семантика, совместимость с screen reader.

Практические рекомендации по обеспечению доступности

Семантический HTML. Используйте правильные теги: <button> для кнопок, <a> для ссылок, <input> для полей ввода. Не используйте <div onclick="..."> вместо <button>.

Альтернативный текст для изображений:

<!-- Информативное изображение -->
<img src="chart.png" alt="График роста продаж: рост на 25% за 2024 год">

<!-- Декоративное изображение -->
<img src="decoration.png" alt="">

ARIA-атрибуты (Accessible Rich Internet Applications). Используются, когда семантический HTML недостаточен:

<div role="alert" aria-live="assertive">
Ошибка: неверный формат email
</div>

<button aria-expanded="false" aria-controls="menu">
Меню
</button>
<ul id="menu" role="menu" hidden>
<li role="menuitem"><a href="/">Главная</a></li>
</ul>

Навигация с клавиатуры. Все интерактивные элементы должны быть фокусируемыми и управляемыми с клавиатуры. Видимый индикатор фокуса обязателен.

Подписи форм:

<!-- Плохо: подпись не связана с полем -->
<div>Email</div>
<input type="email">

<!-- Хорошо: подпись связана программно -->
<label for="email">Email</label>
<input type="email" id="email">

<!-- Или через вложенность -->
<label>
Email
<input type="email">
</label>

Доступность — это не дополнительная функция, а фундаментальное требование к качественному веб-приложению. Понимание принципов a11y показывает зрелость разработчика и его способность создавать инклюзивные продукты.

Вопрос 5. Почему для кнопки лучше использовать тег button, а не div.

Таймкод: 00:05:04

Ответ собеседника: Правильный. У button уже есть встроенные состояния (hover, focus), он фокусируется с клавиатуры (tabindex), а div — это просто блок, который скринридер не распознаёт как кнопку. Для div нужно вручную прописывать все эти поведения и доступность.

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

Кандидат дал отличный ответ, охватив все ключевые аспекты. Это один из лучших ответов в интервью — понимание доступности на таком уровне свидетельствует о хорошей технической подготовке.

Полное сравнение button и div в роли кнопки

Нативная семантика и роль. Тег <button> имеет встроенную ARIA-роль button. Screen reader автоматически объявляет его как кнопку и озвучивает содержимое как имя кнопки. Тег <div> имеет роль generic — screen reader не понимает, что это интерактивный элемент.

Клавиатурная доступность. <button> автоматически фокусируется при нажатии Tab и активируется клавишами Enter и Space. <div> не получает фокус по Tab и не реагирует на клавиатурные события без дополнительной разработки.

Встроенные состояния. <button> имеет нативные состояния: :hover, :focus, :active, :disabled. Для <div> каждое из этих состояний нужно реализовывать вручную.

Поведение в формах. <button> внутри <form> по умолчанию имеет тип submit, что обеспечивает отправку формы при нажатии Enter внутри полей ввода. <div> не имеет такого поведения.

Что нужно реализовать вручную для div, чтобы он работал как кнопка:

<!-- Нативная кнопка — всё работает из коробки -->
<button onclick="handleClick()">Отправить</button>

<!-- Div-кнопка — нужно добавить всё это -->
<div
role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="if(event.key==='Enter'||event.key===' '){handleClick(); event.preventDefault()}"
onfocus="this.classList.add('focused')"
onblur="this.classList.remove('focused')"
aria-disabled="false"
>
Отправить
</div>

Проблемы с div-кнопками:

  • Нужно вручную добавить tabindex="0" для фокусировки.
  • Нужно обрабатывать keydown для Enter и Space (по спецификации кнопка активируется обеими клавишами).
  • Нужно добавить role="button" для screen reader.
  • Нужно реализовать визуальный индикатор фокуса через CSS или JS.
  • Нужно обработать состояние disabled через aria-disabled и предотвратить клики.
  • Нужно предотвратить всплытие события при нажатии Space (чтобы страница не прокручивалась).

Когда div может быть оправдан:

В редких случаях, когда нужен кастомный интерактивный элемент, который не является кнопкой по смыслу (например, таб, переключатель, аккордеон), используется <div> или <span> с соответствующими ARIA-ролями и поведением. Но если элемент выполняет действие — это должна быть <button>.

Принцип выбора элементов:

Используйте семантический HTML-элемент, который соответствует назначению элемента. Это обеспечивает доступность, клавиатурную навигацию и правильное поведение без дополнительного кода. Кастомные элементы с ARIA — это всегда компромисс, требующий значительных усилий для корректной реализации.

Вопрос 6. Что такое микроразметка для поисковиков (schema.org/Open Graph) и как она работает.

Таймкод: 00:07:40

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

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

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

Что такое микроразметка

Микроразметка (structured data) — это способ добавления метаданных в HTML-код страницы для того, чтобы поисковые системы и другие сервисы могли лучше понимать содержимое и отображать его в расширенном виде. Это не про визуальное оформление, а про семантическое описание сущностей на странице.

Основные стандарты и форматы

Schema.org — словарь типов и свойств для структурированных данных, созданный совместно Google, Yahoo, Bing и Yandex. Описывает сущности: статьи, продукты, события, организации, рецепты, отзывы.

Open Graph Protocol (OGP) — стандарт, созданный Facebook, для описания контента при шеринге в социальных сетях. Используется Facebook, LinkedIn, Telegram, WhatsApp и другими.

Форматы представления структурированных данных:

  • JSON-LD (рекомендуемый Google формат) — JavaScript-объект в теге <script>, отделённый от HTML-разметки.
  • Microdata — атрибуты непосредственно в HTML-тегах.
  • RDFa — расширения HTML5 для семантической разметки.

Как работает JSON-LD (основной формат):

<head>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Заголовок статьи",
"author": {
"@type": "Person",
"name": "Иван Иванов"
},
"datePublished": "2024-01-15",
"dateModified": "2024-01-20",
"description": "Краткое описание статьи",
"image": "https://example.com/image.jpg",
"publisher": {
"@type": "Organization",
"name": "Название сайта",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
}
}
</script>
</head>

Open Graph разметка:

<head>
<meta property="og:title" content="Заголовок страницы">
<meta property="og:description" content="Описание страницы">
<meta property="og:image" content="https://example.com/image.jpg">
<meta property="og:url" content="https://example.com/page">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Название сайта">
</head>

Что даёт микроразметка на практике

Расширенные сниппеты (Rich Snippets) в поисковой выдаче. Поисковик показывает дополнительную информацию прямо в результатах поиска:

  • Рейтинг и количество отзывов для продуктов.
  • Время приготовления и калорийность для рецептов.
  • Дату автора и описание для статей.
  • События с датами и местами.

Витрины знаний (Knowledge Graph). Google может отображать информацию о компании или персоне в боковой панели поиска.

Красивые превью при шеринге. Когда пользователь делится ссылкой в социальных сетях, Open Graph разметка определяет, какое изображение, заголовок и описание будут показаны.

Пример продукта с микроразметкой:

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ноутбук ProBook 15",
"image": "https://example.com/laptop.jpg",
"description": "Мощный ноутбук для работы",
"brand": {
"@type": "Brand",
"name": "ProBook"
},
"offers": {
"@type": "Offer",
"price": "59990",
"priceCurrency": "RUB",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "128"
}
}
</script>

Такой продукт в поисковой выдаче покажет цену, наличие на складе и рейтинг со звёздами.

Популярные типы из Schema.org:

  • Article / NewsArticle / BlogPosting — статьи и новости.
  • Product — товары с ценами и отзывами.
  • Event — события с датами и местами.
  • Recipe — рецепты с ингредиентами и временем.
  • Organization / LocalBusiness — организации и бизнесы.
  • Person — персоны.
  • FAQPage — страницы с вопросами и ответами.
  • BreadcrumbList — хлебные крошки.
  • HowTo — пошаговые инструкции.

Проверка микроразметки:

  • Google Rich Results Test — проверка поддерживаемой Google разметки.
  • Schema.org Validator — валидация синтаксиса.
  • Facebook Sharing Debugger — проверка Open Graph для Facebook.

Микроразметка — это мост между HTML-кодом и пониманием контента машинами. Она не влияет напрямую на ранжирование, но значительно улучшает представление сайта в поисковой выдаче и социальных сетях, что повышает кликабельность.

Вопрос 7. Какие системы раскладки в CSS знаешь и когда лучше использовать flex, а когда grid.

Таймкод: 00:09:38

Ответ собеседника: Правильный. Знаю табличную вёрстку, flex и grid. Flex удобен для расположения элементов в одну строку или столбец (списки), grid — для сложных макетов с несколькими строками и колонками.

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

Кандидат верно определил основные случаи использования flex и grid. Это правильное понимание, которое можно дополнить деталями.

Эволюция систем раскладки в CSS

Табличная вёрстка (устаревший подход). Использование <table>, <tr>, <td> для создания макетов. Семантически некорректно — таблицы предназначены для табличных данных, а не для раскладки. Жёсткая структура, сложность адаптации, проблемы с доступностью. Сегодня используется только по прямому назначению — для таблиц с данными.

Float-раскладка (устаревший подход). Свойство float изначально предназначалось для обтекания текстом изображений, но использовалось для создания колонок. Требовало clearfix-хаков и было ненадёжным.

Flexbox (одномерная раскладка). Создан для распределения элементов вдоль одной оси — горизонтальной или вертикальной. Идеален для компонентов и мелких элементов.

CSS Grid (двумерная раскладка). Позволяет управлять элементами одновременно по двум осям — строкам и колонкам. Создан для страничных макетов и сложных сеток.

Flexbox — когда использовать

Flexbox работает в одном измерении: либо строка, либо столбец. Он управляет распределением пространства и выравниванием элементов вдоль одной оси.

Типичные случаи использования flex:

  • Навигационные панели и меню.
  • Центрирование элемента по горизонтали и вертикально.
  • Карточки одинаковой высоты в ряду.
  • Распределение элементов по краям контейнера.
  • Формы с полями и кнопками.
  • Списки элементов.
/* Навигация: логотип слева, меню справа */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
}

/* Центрирование */
.center {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

/* Карточки одинаковой высоты */
.card-list {
display: flex;
gap: 16px;
}
.card {
flex: 1; /* все карточки одинаковой ширины */
}

CSS Grid — когда использовать

Grid работает в двух измерениях одновременно: строки и колонки. Позволяет размещать элементы в любой ячейке сетки, растягивать на несколько ячеек, создавать сложные макеты.

Типичные случаи использования grid:

  • Основной макет страницы (шапка, сайдбар, контент, подвал).
  • Галереи и карточные сетки с неправильной структурой.
  • Макеты, где элементы должны занимать несколько колонок или строк.
  • Сложные формы с разным размером полей.
/* Макет страницы */
.page {
display: grid;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}

.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }

/* Адаптивная карточная сетка */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}

Комбинирование flex и grid

На практике flex и grid используются вместе. Grid — для общей структуры страницы, flex — для внутренней раскладки компонентов:

/* Grid для макета страницы */
.layout {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 60px 1fr 80px;
}

/* Flex для навигации внутри шапки */
.header {
display: flex;
justify-content: space-between;
align-items: center;
}

/* Flex для карточки */
.card {
display: flex;
flex-direction: column;
}
.card-content {
flex: 1; /* занимает всё доступное пространство */
}

Ключевое правило выбора:

  • Одномерная раскладка (элементы в линию или столбец) — Flexbox.
  • Двумерная раскладка (строки и колонки одновременно) — Grid.
  • Компонент (кнопка, карточка, навигация) — Flexbox.
  • Макет страницы (шапка, сайдбар, контент) — Grid.

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

Вопрос 8. Чем отличаются псевдоклассы от псевдоэлементов.

Таймкод: 00:10:47

Ответ собеседника: Неполный. Псевдоэлементы — это вроде before и after, можно задать контент. Псевдоклассы — когда хотим стилизовать элемент в определённом состоянии, но не смогла чётко сформулировать разницу.

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

Кандидат интуитивно понимает разницу, но не смог чётко артикулировать. Это базовый вопрос по CSS, на который стоит уметь отвечать структурированно.

Псевдоклассы (Pseudo-classes)

Псевдокласс — это ключевое слово, добавляемое к селектору, которое определяет особое состояние элемента. Псевдокласс отвечает на вопрос «в каком состоянии находится элемент?».

Синтаксис: одно двоеточие — :hover, :focus, :nth-child(2).

Категории псевдоклассов:

Состояния взаимодействия:

a:hover { color: red; } /* наведение курсора */
a:active { color: blue; } /* момент нажатия */
a:focus { outline: 2px solid; } /* элемент в фокусе */
a:visited { color: purple; } /* посещённая ссылка */

Состояния элементов формы:

input:disabled { opacity: 0.5; } /* заблокированное поле */
input:checked { accent-color: green; } /* отмеченный чекбокс */
input:required { border-color: red; } /* обязательное поле */
input:valid { border-color: green; } /* валидное значение */
input:invalid { border-color: red; } /* невалидное значение */
input:focus-within { box-shadow: 0 0 4px blue; } /* фокус внутри контейнера */

Структурные псевдоклассы:

li:first-child { font-weight: bold; } /* первый дочерний элемент */
li:last-child { margin-bottom: 0; } /* последний дочерний элемент */
li:nth-child(odd) { background: #f5f5f5; } /* нечётные элементы */
li:nth-child(2n) { background: #f5f5f5; } /* каждый второй элемент */
li:nth-child(3n+1) { color: red; } /* каждый третий, начиная с первого */
p:first-of-type { font-size: 1.2em; } /* первый абзац среди siblings */
p:last-of-type { margin-bottom: 0; } /* последний абзац */
:empty { display: none; } /* пустой элемент */
:root { --main-color: #333; } /* корневой элемент (html) */

Логические псевдоклассы:

:not(.hidden) { display: block; } /* все элементы без класса .hidden */
:is(h1, h2, h3) { font-family: serif; } /* любой из перечисленных */
:has(> img) { padding: 10px; } /* содержит прямой дочерний img */

Псевдоэлементы (Pseudo-elements)

Псевдоэлемент — это ключевое слово, добавляемое к селектору, которое позволяет стилизовать определённую часть элемента или создать виртуальный элемент, которого нет в DOM. Псевдоэлемент отвечает на вопрос «какую часть элемента стилизовать?».

Синтаксис: два двоеточия — ::before, ::after (по спецификации CSS3, хотя одно двоеточие тоже работает для обратной совместимости).

Основные псевдоэлементы:

::before и ::after — создают виртуальные элементы перед и после содержимого. Обязательное свойство — content.

/* Декоративная иконка перед ссылкой */
.external-link::before {
content: "→ ";
color: blue;
}

/* Кавычки для цитаты */
blockquote::before {
content: open-quote;
font-size: 2em;
color: gray;
}
blockquote::after {
content: close-quote;
font-size: 2em;
color: gray;
}

/* Очистка float (clearfix) */
.clearfix::after {
content: "";
display: table;
clear: both;
}

::first-line — стилизует первую строку текстового блока:

p::first-line {
font-weight: bold;
font-size: 1.1em;
}

::first-letter — стилизует первую букву (буквица):

p::first-letter {
font-size: 3em;
float: left;
margin-right: 8px;
color: darkred;
}

::placeholder — стилизует текст подсказки в поле ввода:

input::placeholder {
color: #999;
font-style: italic;
}

::selection — стилизует выделенный пользователем текст:

::selection {
background: #333;
color: white;
}

::marker — стилизует маркер списка:

li::marker {
color: red;
content: "✓ ";
}

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

ХарактеристикаПсевдоклассПсевдоэлемент
Что делаетОписывает состояние элементаСоздаёт/стилизует часть элемента
Синтаксис:hover, :focus::before, ::after
Вопрос«В каком состоянии?»«Какую часть?»
ПримерНаведённая ссылка, отмеченный чекбоксПервая строка, декоративный элемент
Влияет наСуществующий элементВиртуальную часть элемента

Пример, объединяющий оба подхода:

/* Псевдокласс: состояние наведения */
.button:hover {
background: #0056b3;
}

/* Псевдоэлемент: декоративная стрелка */
.button::after {
content: " →";
}

/* Комбинация: стрелка меняет цвет при наведении */
.button:hover::after {
content: " →";
color: yellow;
}

Псевдоклассы и псевдоэлементы — мощные инструменты CSS, которые позволяют создавать интерактивные и декоративные эффекты без добавления лишних элементов в HTML.

Вопрос 9. TypeScript или JavaScript и почему.

Таймкод: 00:11:33

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

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

Кандидат дал эмоциональный, но по сути правильный ответ. Для полноты картины стоит раскрыть тему более структурированно.

TypeScript — это JavaScript с системой типов. TypeScript является надмножеством (superset) JavaScript: любой валидный JavaScript-код является валидным TypeScript-кодом. TypeScript компилируется (транспилируется) в обычный JavaScript и добавляет статическую типизацию.

Преимущества TypeScript

Раннее обнаружение ошибок. TypeScript находит ошибки на этапе компиляции, а не во время выполнения. Это предотвращает целый класс багов, типичных для JavaScript:

// JavaScript — ошибка обнаружится только в runtime
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet(null); // Runtime error: Cannot read property 'toUpperCase' of null

// TypeScript — ошибка обнаружится до запуска
function greet(name: string): string {
return "Hello, " + name.toUpperCase();
}
greet(null); // Compile error: Argument of type 'null' is not assignable to parameter of type 'string'

Автодополнение и IntelliSense. IDE точно знает типы переменных, свойства объектов и сигнатуры функций. Это ускоряет разработку и снижает количество ошибок.

Самодокументируемый код. Типы служат документацией, которая не устаревает (в отличие от комментариев):

// Понятно, какие параметры ожидаются и что возвращается
function createUser(name: string, age: number, role: UserRole): User {
// ...
}

// Интерфейс описывает структуру данных
interface User {
id: string;
name: string;
email: string;
age: number;
role: UserRole;
createdAt: Date;
}

enum UserRole {
Admin = 'admin',
User = 'user',
Moderator = 'moderator',
}

Безопасный рефакторинг. При переименовании свойства или изменении сигнатуры функции TypeScript покажет все места, которые нужно обновить. В JavaScript пришлось бы искать вручную и рисковать пропустить что-то.

Продвинутая система типов. TypeScript предлагает возможности, которых нет в большинстве языков:

// Union types
type Status = 'pending' | 'success' | 'error';

// Intersection types
type AdminUser = User & { permissions: string[] };

// Generics
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}

// Mapped types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

// Conditional types
type IsString<T> = T extends string ? true : false;

// Template literal types
type EventName = `on${Capitalize<string>}`;

// Type guards
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj;
}

Когда JavaScript может быть предпочтительнее

Маленькие проекты и скрипты. Для простого лендинга или небольшого скрипта настройка TypeScript может быть избыточной.

Быстрое прототипирование. На этапе экспериментов, когда типы ещё не устоялись, JavaScript позволяет быстрее итерировать.

Команда без опыта TypeScript. Если команда не знакома с системой типов, внедрение TypeScript может замедлить разработку.

Сборка без шага компиляции. Современные инструменты (esbuild, Vite) позволяют использовать TypeScript без отдельного шага компиляции, снимая этот аргумент.

Практический вывод

Для серьёзных проектов TypeScript практически всегда предпочтительнее JavaScript. Статическая типизация снижает количество багов, улучшает читаемость кода, ускоряет онбординг новых разработчиков и делает рефакторинг безопасным. По данным исследований, TypeScript позволяет обнаружить до 15% багов до попадания кода в production.

Для Golang-разработчика переход на TypeScript обычно прост, так как Go — статически типизированный язык, и концепция типизации знакома.

Вопрос 10. Почему лучше TypeScript, чем JavaScript.

Таймкод: 00:11:52

Ответ собеседника: Правильный. TypeScript подсказывает типы, сразу видно ошибки при написании кода, не нужно писать лишнее — нажал точку и он показал все свойства. Удобно работать с интерфейсами — сразу понятно, что приходит с бэкенда. Для больших проектов без TypeScript невозможно работать.

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

Этот вопрос по сути повторяет предыдущий. Кандидат дал хороший ответ, дополнив его конкретными примерами из практики. Поскольку подробный ответ уже был дан в вопросе 9, здесь можно кратко резюмировать ключевые моменты.

Краткий ответ:

TypeScript предпочтительнее JavaScript для больших проектов по нескольким причинам:

  • Статическая типизация — ошибки обнаруживаются на этапе компиляции, а не в runtime.
  • Автодополнение и IntelliSense — IDE знает типы и предлагает правильные свойства и методы.
  • Самодокументируемость — типы описывают структуру данных и контракты между модулями.
  • Безопасный рефакторинг — компилятор покажет все места, затронутые изменением.
  • Удобная работа с API — интерфейсы описывают формат данных с бэкенда, исключая ошибки при обработке ответов.

Для серьёзной разработки TypeScript стал стандартом де-факто, и работа без него в больших проектах действительно затруднительна.

Вопрос 11. Чем отличается type от interface в TypeScript и когда что использовать.

Таймкод: 00:13:10

Ответ собеседника: Правильный. Отличаются нотацией — interface с фигурными скобками, type через знак равно. Через type можно объединять типы с помощью амперсанда (&), через interface — через extends. Принципиальная разница: type позволяет создавать union типы (string | number), а interface — нет.

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

Кандидат отлично разбирается в теме и дал точный ответ. Это один из лучших ответов в интервью — глубокое понимание TypeScript.

Полное сравнение type и interface

Синтаксис:

// Interface
interface User {
id: string;
name: string;
email: string;
}

// Type
type User = {
id: string;
name: string;
email: string;
};

Объединение типов:

// Type — пересечение (intersection)
type Admin = User & {
permissions: string[];
};

// Interface — расширение (extends)
interface Admin extends User {
permissions: string[];
}

Union типы — только type:

// Union — доступен только через type
type Status = 'pending' | 'success' | 'error';
type ID = string | number;
type Result<T> = { success: true; data: T } | { success: false; error: string };

// Interface не может быть union
// Это невозможно:
// interface Status = 'pending' | 'success' | 'error'; // Ошибка

Объявление (declaration merging) — только interface:

// Interface можно объявить несколько раз — они объединятся
interface User {
id: string;
}
interface User {
name: string;
}
// Результат: User имеет свойства id и name

// Type нельзя объявить дважды
type ID = string;
type ID = number; // Error: Duplicate identifier 'ID'

Это свойство interface используется в библиотеках для расширения глобальных типов (например, Window, Express.Request).

Примитивы и алиасы — только type:

type Name = string;
type ID = string | number;
type Callback = (error: Error | null, result: unknown) => void;

// Interface не может описывать примитивы
// interface Name = string; // Ошибка

Mapped types и условные типы — только type:

// Mapped type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

// Conditional type
type IsString<T> = T extends string ? true : false;

// Template literal type
type EventName = `on${Capitalize<string>}`;

Когда использовать interface:

  • Описание объектов и их структуры.
  • Когда нужна возможность расширения через extends.
  • Для описания публичного API библиотеки (declaration merging).
  • Когда важна производительность компилятора (interface работает быстрее при проверке extends).
  • Для описания классов (implements).

Когда использовать type:

  • Union и intersection типы.
  • Примитивные алиасы.
  • Mapped types, conditional types, template literal types.
  • Типы, основанные на других типах (вычисляемые типы).
  • Кортежи: type Point = [number, number].

Практическая рекомендация:

Большинство разработчиков рекомендуют использовать interface по умолчанию для описания объектов и переключаться на type, когда нужны его уникальные возможности (union, mapped types, conditional types). Это создаёт предсказуемую базу кода и упрощает принятие решений.

Вопрос 12. Что такое enum и когда его удобно использовать.

Таймкод: 00:14:52

Ответ собеседника: Правильный. Enum нужен для зафиксированного списка констант, который практически не меняется. Удобно, когда приходят кодовые значения (например, две буквы или числа 0/1), а можно преобразовать их в понятные строковые значения. Также при использовании enum в функциях IDE сразу подсказывает доступные варианты.

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

Кандидат отлично понимает enum и приводит практические примеры использования. Это сильный ответ, демонстрирующий опыт работы с TypeScript.

Что такое enum

Enum (перечисление) — это специальный тип данных в TypeScript, который позволяет определить набор именованных констант. Enum делает код более читаемым и безопасным, замая «магические числа» и строки на понятные имена.

Виды enum в TypeScript

Числовые enum:

enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}

// Можно задать начальное значение
enum StatusCode {
OK = 200,
BadRequest = 400,
NotFound = 404,
InternalError = 500
}

// Использование
function move(direction: Direction) {
switch (direction) {
case Direction.Up:
// ...
break;
case Direction.Down:
// ...
break;
}
}

move(Direction.Up); // Читаемо и безопасно

Строковые enum:

enum UserRole {
Admin = 'admin',
User = 'user',
Moderator = 'moderator'
}

enum OrderStatus {
Pending = 'PENDING',
Confirmed = 'CONFIRMED',
Shipped = 'SHIPPED',
Delivered = 'DELIVERED',
Cancelled = 'CANCELLED'
}

// Преимущество: при сериализации/десериализации сохраняется понятное значение
const status: OrderStatus = OrderStatus.Pending;
console.log(status); // "PENDING", а не 0 или 1

Константные enum (const enum):

const enum Colors {
Red = '#FF0000',
Green = '#00FF00',
Blue = '#0000FF'
}

// Компилятор полностью удаляет const enum и подставляет значения
const color = Colors.Red; // После компиляции: const color = "#FF0000"

Const enum не генерирует объект во время выполнения — значения подставляются inline. Это уменьшает размер бандла, но не позволяет итерировать по значениям.

Когда удобно использовать enum

Статусы и состояния:

enum PaymentStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Failed = 'failed',
Refunded = 'refunded'
}

function handlePayment(status: PaymentStatus) {
if (status === PaymentStatus.Completed) {
sendReceipt();
}
}

Коды ошибок и ответов API:

enum ErrorCode {
InvalidInput = 1000,
Unauthorized = 1001,
Forbidden = 1002,
NotFound = 1003,
RateLimited = 1004
}

Роли и права пользователей:

enum Permission {
Read = 'read',
Write = 'write',
Delete = 'delete',
Admin = 'admin'
}

function checkPermission(user: User, permission: Permission): boolean {
return user.permissions.includes(permission);
}

Маппинг внешних кодов:

// Внешний API присылает коды стран как числа
enum CountryCode {
Russia = 7,
USA = 1,
Germany = 49,
Japan = 81
}

Альтернатива enum — as const:

В современном TypeScript часто используют объекты с as const вместо enum:

const OrderStatus = {
Pending: 'PENDING',
Confirmed: 'CONFIRMED',
Shipped: 'SHIPPED',
Delivered: 'DELIVERED',
} as const;

type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];

Этот подход не генерирует дополнительный код при компиляции и работает с tree-shaking, но не поддерживает reverse mapping (получение имени по значению), как числовые enum.

Когда не стоит использовать enum:

  • Когда набор значений может расширяться динамически.
  • Когда важна минимальная сборка и не нужны дополнительные объекты (используйте const enum или as const).
  • Когда значения приходят из внешнего источника (API, база данных) и не известны на этапе компиляции.

Вопрос 13. Что такое дженерики (generics) и зачем они нужны.

Таймкод: 00:16:26

Ответ собеседника: Правильный. Дженерики — это параметр типа (T) в угловых скобках. Позволяют создавать универсальные функции, которые работают с разными типами. Можно ограничить дженерик через extends, чтобы принимать только определённые интерфейсы. На практике объединяла три похожие функции в одну с дженериком.

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

Кандидат отлично понимает дженерики и подтверждает это практическим примером. Это один из лучших ответов в интервью.

Что такое дженерики

Дженерики (generics) — это механизм, позволяющий создавать компоненты, которые работают с различными типами данных, сохраняя при этом типобезопасность. По сути, дженерики — это параметры для типов, аналогично тому, как параметры функции позволяют передавать разные значения.

Проблема без дженериков:

// Без дженериков — три одинаковые функции для разных типов
function getFirstNumber(arr: number[]): number | undefined {
return arr[0];
}

function getFirstString(arr: string[]): string | undefined {
return arr[0];
}

function getFirstBoolean(arr: boolean[]): boolean | undefined {
return arr[0];
}

// Или использование any — потеря типобезопасности
function getFirst(arr: any[]): any {
return arr[0];
}
const result = getFirst([1, 2, 3]);
result.toUpperCase(); // Нет ошибки компиляции, но будет ошибка в runtime

Решение с дженериками:

// Одна универсальная функция для любого типа
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}

const num = getFirst([1, 2, 3]); // Тип: number | undefined
const str = getFirst(['a', 'b', 'c']); // Тип: string | undefined

// TypeScript автоматически выводит тип
const num2 = getFirst<number>([1, 2, 3]); // Явное указание типа

Ограничение дженериков через extends:

interface HasLength {
length: number;
}

// T должен иметь свойство length
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}

logLength('hello'); // OK: string имеет length
logLength([1, 2, 3]); // OK: array имеет length
logLength({ length: 5 }); // OK: объект имеет length
// logLength(123); // Error: number не имеет length

Дженерики в интерфейсах и типах:

interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

interface Repository<T, ID> {
findById(id: ID): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: ID): Promise<void>;
}

// Использование
type UserResponse = ApiResponse<User>;
type UserRepository = Repository<User, string>;

Дженерики в классах:

class Queue<T> {
private items: T[] = [];

enqueue(item: T): void {
this.items.push(item);
}

dequeue(): T | undefined {
return this.items.shift();
}

peek(): T | undefined {
return this.items[0];
}
}

const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const first = numberQueue.dequeue(); // Тип: number | undefined

Продвинутые паттерны с дженериками:

// Ограничение по ключам объекта
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // Тип: string
// getProperty(user, 'email'); // Error: 'email' не является ключом User

// Условные типы с дженериками
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<string>; // false

// Mapped types с дженериками
type Partial<T> = {
[P in keyof T]?: T[P];
};

type Required<T> = {
[P in keyof T]-?: T[P];
};

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Практический пример — API клиент:

interface HttpClient {
get<T>(url: string): Promise<T>;
post<T, D>(url: string, data: D): Promise<T>;
put<T, D>(url: string, data: D): Promise<T>;
delete(url: string): Promise<void>;
}

// Использование
const user = await httpClient.get<User>('/api/users/1');
const createdUser = await httpClient.post<User, CreateUserDto>('/api/users', newUser);

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

Вопрос 14. Какие хуки React знаешь и для чего они используются.

Таймкод: 00:19:19

Ответ собеседника: Правильный. useState — возвращает состояние и функцию для его изменения, при изменении состояния компонент перерисовывается. useEffect — для побочных эффектов, например загрузки данных. Можно указать зависимости — при их изменении эффект перезапускается. Пустой массив зависимостей — выполнится один раз. Для отписки используется cleanup-функция (return).

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

Кандидат отлично описал useState и useEffect — два основных хука. Для полноты картины стоит дополнить ответ другими важными хуками.

Основные хуки React

useState — управление состоянием компонента:

const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);

// Функциональное обновление (когда новое значение зависит от предыдущего)
setCount(prev => prev + 1);

// Ленивая инициализация (вычисление начального состояния только при первом рендере)
const [data, setData] = useState(() => {
const cached = localStorage.getItem('data');
return cached ? JSON.parse(cached) : [];
});

useEffect — побочные эффекты:

// Запускается при каждом рендере
useEffect(() => {
console.log('Component rendered');
});

// Запускается один раз при монтировании
useEffect(() => {
fetchData();
}, []);

// Запускается при изменении зависимостей
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);

// Cleanup-функция (отписка при размонтировании)
useEffect(() => {
const subscription = eventBus.subscribe('update', handleUpdate);
const timer = setInterval(tick, 1000);

return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, []);

Другие важные хуки:

useRef — хранение мутабельного значения без ре-рендера:

const inputRef = useRef<HTMLInputElement>(null);
const renderCount = useRef(0);

// Доступ к DOM-элементу
const focusInput = () => inputRef.current?.focus();

// Хранение предыдущего значения
const prevValue = useRef(value);
useEffect(() => {
prevValue.current = value;
}, [value]);

useMemo — мемоизация вычислений:

const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // Пересчитывается только при изменении items

useCallback — мемоизация функций:

const handleClick = useCallback((id: string) => {
dispatch({ type: 'delete', payload: id });
}, [dispatch]); // Функция пересоздаётся только при изменении dispatch

useContext — доступ к контексту без Consumer:

const theme = useContext(ThemeContext);
const user = useContext(AuthContext);

useReducer — сложное состояние:

type State = { count: number; step: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'setStep'; payload: number };

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step };
case 'decrement': return { ...state, count: state.count - state.step };
case 'setStep': return { ...state, step: action.payload };
default: return state;
}
}

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

useId — уникальный идентификатор (React 18+):

const id = useId(); // Генерирует стабильный уникальный ID
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);

Кандидат продемонстрировал глубокое понимание основных хуках. Знание cleanup-функции в useEffect особенно важно — это показывает понимание утечек памяти в React-приложениях.

Вопрос 15. В чём преимущество функциональных компонентов с хуками перед классовыми.

Таймкод: 00:21:55

Ответ собеседника: Правильный. С хуками можно разбить большой state на маленькие независимые кусочки, каждый со своим значением. В классовых компонентах был один огромный state. Это упрощает управление состоянием и переиспользование логики.

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

Кандидат верно отметил ключевое преимущество — декомпозицию состояния. Для полноты картины стоит раскрыть и другие преимущества.

Основные преимущества функциональных компонентов с хуками

Переиспользуемая логика через кастомные хуки. Это, пожалуй, главное преимущество. В классовых компонентах для переиспользования логики приходилось использовать сложные паттерны: Higher-Order Components (HOC), render props, миксины. Хуки позволяют извлечь логику в простую функцию:

// Кастомный хук — переиспользуемая логика
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debouncedValue;
}

// Использование в любом компоненте
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);

useEffect(() => {
searchAPI(debouncedQuery);
}, [debouncedQuery]);

return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

function AnotherComponent() {
const [value, setValue] = useState('');
const debouncedValue = useDebounce(value, 500); // Тот же хук, другой delay
}

Декомпозиция логики по смыслу, а не по методам жизненного цикла. В классовых компонентах логика размазывалась по методам componentDidMount, componentDidUpdate, componentWillUnmount:

// Классовый компонент — логика размазана
class UserProfile extends Component {
componentDidMount() {
this.fetchUser(this.props.userId); // Загрузка пользователя
document.title = 'User Profile'; // Установка заголовка
this.subscription = eventBus.subscribe(); // Подписка
}

componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId); // Та же логика, но в другом методе
}
}

componentWillUnmount() {
this.subscription.unsubscribe(); // Отписка
}
}

// Функциональный компонент — логика сгруппирована по смыслу
function UserProfile({ userId }: Props) {
useEffect(() => {
fetchUser(userId);
}, [userId]); // Вся логика загрузки пользователя в одном месте

useEffect(() => {
document.title = 'User Profile';
}, []); // Логика заголовка отдельно

useEffect(() => {
const subscription = eventBus.subscribe();
return () => subscription.unsubscribe();
}, []); // Логика подписки отдельно
}

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

// Классовый компонент — проблема с this
class Counter extends Component {
state = { count: 0 };

// Нужно привязыть или использовать arrow function
increment() {
this.setState({ count: this.state.count + 1 }); // this может быть undefined
}

// Использование стрелочной функции в render — создаёт новую функцию при каждом рендере
render() {
return <button onClick={() => this.increment()}>+1</button>;
}
}

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

Меньше бойлерплейта. Функциональные компоненты короче и проще:

// Классовый компонент — 15+ строк для простого компонента
class Welcome extends Component {
constructor(props) {
super(props);
this.state = { name: '' };
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

// Функциональный компонент — 3 строки
function Welcome({ name }: Props) {
return <h1>Hello, {name}</h1>;
}

Отсутствие HOC и render props. До появления хуков для переиспользования логики использовались HOC и render props, которые усложняли дерево компонентов и создавали проблему «wrapper hell»:

// До хуков — wrapper hell
export default withRouter(
withAuth(
withTheme(
withLoading(UserProfile)
)
)
);

// После хуков — чистый компонент
function UserProfile() {
const router = useRouter();
const { user } = useAuth();
const theme = useTheme();
const { isLoading } = useLoading();
}

Лучшая оптимизация. React.memo для функциональных компонентов проще в использовании, чем PureComponent или shouldComponentUpdate для классовых.

Современный стандарт. React развивается в сторону функциональных компонентов. Новые возможности (Suspense, Concurrent Mode, Server Components) работают только с функциональными компонентами и хуками.

Кандидат правильно отметил, что хуки позволяют разбить состояние на независимые части — это фундаментальное изменение в подходе к управлению состоянием по сравнению с единым this.state в классовых компонентах.

Вопрос 16. Чем отличается useMemo от useCallback.

Таймкод: 00:22:40

Ответ собеседника: Правильный. useMemo возвращает мемоизированное значение, а useCallback возвращает мемоизированную функцию. Если данные в зависимостях не меняются, то значение/функция не пересчитываются.

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

Кандидат дал точный и лаконичный ответ. Это понимание на уровне эксперта — коротко и по сути.

Детальное сравнение

useMemo мемоизирует результат вычисления (значение):

const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]); // Возвращает отсортированный массив

const totalPrice = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]); // Возвращает число

useCallback мемоизирует саму функцию (ссылку на функцию):

const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // Возвращает функцию

const handleSubmit = useCallback((data: FormData) => {
dispatch(submitAction(data));
}, [dispatch]); // Возвращает функция

Ключевое понимание: useCallback(fn, deps) эквивалентен useMemo(() => fn, deps). useCallback — это синтаксический сахар для мемоизации функций:

// Эти два выражения эквивалентны
const handleClick = useCallback(() => {
doSomething();
}, [dep]);

const handleClick = useMemo(() => {
return () => doSomething();
}, [dep]);

Когда использовать:

useMemo:

  • Дорогие вычисления (сортировка, фильтрация больших массивов).
  • Создание объектов/массивов, которые передаются как пропсы в мемоизированные дочерние компоненты.
  • Значения, используемые в зависимостях других хуков.

useCallback:

  • Функции, передаваемые в дочерние компоненты, обёрнутые в React.memo.
  • Функции, используемые как зависимости в useEffect или других хуках.
  • Обработчики событий в списках (предотвращение лишних ре-рендеров).

Практический пример:

function TodoList({ todos }: Props) {
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');

// useMemo — мемоизируем вычисление
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}, [todos, filter]);

// useCallback — мемоизируем функцию для дочернего компонента
const handleToggle = useCallback((id: string) => {
// ...
}, []);

return (
<>
<FilterButtons filter={filter} setFilter={setFilter} />
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</>
);
}

// Дочерний компонент — React.memo предотвращает ре-рендер при неизменных пропсах
const TodoItem = React.memo(function TodoItem({ todo, onToggle }: ItemProps) {
return <li onClick={() => onToggle(todo.id)}>{todo.text}</li>;
});

Если бы onToggle не был обёрнут в useCallback, при каждом ре-рендере TodoList создавалась бы новая функция, React.memo в TodoItem не помог бы, и все элементы списка перерисовывались бы заново.

Вопрос 17. Что такое виртуальный DOM в React и зачем он нужен.

Таймкод: 00:23:33

Ответ собеседника: Правильный. Виртуальный DOM — это копия DOM-дерева в памяти. Операции с реальным DOM очень тяжёлые, страница бы долго обновлялась. React сравнивает виртуальное DOM-дерево до и после изменений и перерисовывает только изменившиеся элементы, что улучшает производительность.

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

Кандидат отлично объяснил концепцию виртуального DOM. Это точный и полный ответ.

Что такое виртуальный DOM

Виртуальный DOM (Virtual DOM) — это легковесное JavaScript-представление реального DOM-дерева. Это обычный JavaScript-объект, который описывает структуру UI: какие элементы есть, какие у них свойства и дочерние элементы.

Как работает процесс:

Создание виртуального DOM. Когда React-компонент рендерится, он возвращает JSX, который через React.createElement превращается в объекты виртуального DOM:

// JSX
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>

// Результат React.createElement — объект виртуального DOM
{
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } },
{ type: 'p', props: { children: 'World' } }
]
}
}

Reconciliation (согласование). При изменении состояния React создаёт новое виртуальное DOM-дерево и сравнивает его с предыдущим. Этот процесс называется reconciliation (согласование) или diffing.

Патчинг реального DOM. React вычисляет минимальный набор изменений и применяет их к реальному DOM. Обновляются только те элементы, которые действительно изменились.

Почему это эффективно

Реальный DOM медленный. Каждое изменение реального DOM вызывает цепочку операций: пересчёт стилей (style recalculation), расчёт расположения элементов (layout/reflow), отрисовку (paint), композитинг (composite). Эти операции дорогие, особенно если изменять DOM часто и по частям.

Батчинг обновлений. React собирает все изменения за один цикл и применяет их к реальному DOM одним батчем, а не по одному за раз. Это значительно сокращает количество дорогих операций с DOM.

Алгоритм сравнения (diffing algorithm). React использует эвристический алгоритм сравнения с двумя ключевыми допущениями:

  • Элементы разных типов дают разные деревья (вместо сравнения <div> и <span> React уничтожает старое дерево и создаёт новое).
  • Ключ (key) указывает, какие элементы стабильны между рендерами.

Пример оптимизации:

// Без ключей — React пересоздаёт все элементы списка при добавлении в начало
{todos.map(todo => <TodoItem todo={todo} />)}

// С ключами — React понимает, что новый элемент добавился в начало,
// а остальные остались на месте
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

Важное уточнение

Виртуальный DOM — не самоцель, а деталь реализации React. Главная ценность React — декларативная модель программирования: вы описываете, как должен выглядеть UI для каждого состояния, а React эффективно обновляет DOM, чтобы он соответствовал этому описанию. Виртуальный DOM — механизм, который делает это эффективным.

Стоит также отметить, что другие фреймворки (Vue, Svelte) используют другие подходы. Svelte, например, вообще не использует виртуальный DOM — он компилирует компоненты в оптимизированный JavaScript-код, который напрямую обновляет DOM.

Вопрос 18. Какой state-менеджер использовала в React и чем отличается Redux от Recoil.

Таймкод: 00:25:25

Ответ собеседника: Правильный. Использовала Recoil. Recoil проще, подходит для небольших приложений. Redux — популярнее, дольше на рынке, подходит для больших приложений с огромным state, где нужна структура через actions и reducers.

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

Кандидат верно описал оба менеджера и дал практическую оценку. Для полноты картины стоит дополнить техническими деталями.

Redux — предсказуемый контейнер состояния

Redux основан на трёх принципах: единственный источник истины (store), состояние только для чтения (изменения через actions), изменения через чистые функции (reducers).

// Action
const increment = { type: 'counter/increment' };
const decrement = { type: 'counter/decrement' };

// Reducer
function counterReducer(state = { count: 0 }, action: Action) {
switch (action.type) {
case 'counter/increment': return { count: state.count + 1 };
case 'counter/decrement': return { count: state.count - 1 };
default: return state;
}
}

// Store
const store = createStore(counterReducer);

// Использование
store.dispatch(increment);
const state = store.getState();

Современный Redux (Redux Toolkit) упрощает работу:

import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: state => { state.count += 1; },
decrement: state => { state.count -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.count += action.payload;
},
},
});

const store = configureStore({ reducer: counterSlice.reducer });

Recoil — атомарный подход к состоянию

Recoil создан Facebook специально для React. Основная идея — разбить состояние на атомы (atoms), которые компоненты могут подписываться независимо друг от друга.

import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

// Атом — единица состояния
const countState = atom({
key: 'count',
default: 0,
});

// Селектор — производное состояние
const doubledCountState = selector({
key: 'doubledCount',
get: ({ get }) => get(countState) * 2,
});

// Использование в компоненте
function Counter() {
const [count, setCount] = useRecoilState(countState);
const doubledCount = useRecoilValue(doubledCountState);

return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}

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

ХарактеристикаReduxRecoil
МодельЦентрализованный storeАтомы, распределённые по приложению
ОбновленияActions → ReducersПрямое обновление атомов
Производные данныеСелекторы (reselect)Селекторы (встроенные)
РендерингПерерисовка всех подписчиков при изменении части storeПерерисовка только компонентов, подписанных на конкретный атом
БойлерплейтБольше (actions, reducers, middleware)Меньше
DevToolsОтличные (Redux DevTools)Базовые
ЭкосистояОгромнаяМаленькая
ЗрелостьЗрелый, production-readyЭкспериментальный (на момент создания)

Когда выбрать Redux:

  • Большое приложение со сложным состоянием.
  • Нужна предсказуемость и отладка (Redux DevTools с time-travel debugging).
  • Большая команда, где важна строгая структура.
  • Нужны middleware (redux-thunk, redux-saga) для сложной асинхронной логики.

Когда выбрать Recoil:

  • Среднее приложение, где не нужна строгая структура Redux.
  • Важна гранулярная перерисовка (только подписчики конкретного атома).
  • Хотите минимальный бойлерплейт.
  • Проект на React, и хотите нативную интеграцию.

Альтернативы:

  • Zustand — минималистичный, без boilerplate, набирает популярность.
  • Jotai — атомарный подход, как Recoil, но проще.
  • MobX — реактивный подход с автоматическим отслеживанием зависимостей.
  • Context API — встроенный в React, подходит для простых случаев.

Кандидат правильно отметила, что Recoil проще, а Redux подходит для больших приложений — это практически верная оценка.

Вопрос 19. Что такое SSR (серверный рендеринг) и чем отличается от клиентского рендеринга.

Таймкод: 00:26:33

Ответ собеседника: Правильный. При клиентском рендеринге браузер получает пустую страницу, потом загружается JS-бандл, парсится, исполняется и строится DOM. Это плохо для поисковых роботов и слабых устройств. При SSR сервер уже строит DOM-дерево и отдаёт готовый HTML с данными — поисковики могут индексировать контент, а пользователь видит страницу быстрее. Процесс обогащения статического HTML на клиенте называется гидратация.

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

Кандидат дала отличный ответ, затронув все ключевые аспекты: разницу в процессе, влияние на SEO, производительность и гидратацию. Это один из лучших ответов в интервью.

Детальное сравнение подходов

CSR (Client-Side Rendering) — клиентский рендеринг:

Браузер → Запрос → Сервер → Пустой HTML + JS-бандл
→ Браузер загружает JS → Парсит → Исполняет → Строит DOM → Страница видна

Пользователь видит пустую страницу или спиннер до тех пор, пока JavaScript не загрузится и не выполнится. Поисковые роботы могут не дождаться рендеринга и проиндексировать пустую страницу.

SSR (Server-Side Rendering) — серверный рендеринг:

Браузер → Запрос → Сервер → Сервер рендерит React-компоненты в HTML → Полный HTML
→ Браузер отображает HTML → Загружает JS → Гидратация → Страница интерактивна

Пользователь сразу видит контент. Поисковые роботы получают готовый HTML с данными.

Гидратация (Hydration):

Гидратация — процесс, при котором React на клиенте «привязывается» к уже существующему HTML, создаёт виртуальное DOM-дерево, привязывает обработчики событий и делает страницу интерактивной. После гидратации приложение работает как обычное SPA.

Проблема гидратации: если HTML от сервера не совпадает с тем, что ожидает React на клиенте, возникают ошибки гидратации. Это одна из сложностей при работе с SSR.

SSG (Static Site Generation) — статическая генерация:

Сборка → Все страницы рендерятся в статические HTML-файлы → CDN
→ Браузер получает готовый HTML мгновенно

Страницы генерируются один раз при сборке и отдаются как статические файлы. Подходит для контента, который не меняется часто: блоги, документация, лендинги.

ISR (Incremental Static Regeneration) — инкрементальная регенерация:

Гибридный подход: страницы генерируются статически, но могут перегенерироваться на сервере по расписанию или по запросу. Реализован в Next.js.

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

ХарактеристикаCSRSSRSSG
Первый контентПосле загрузки JSСразуСразу
SEOПлохоеОтличноеОтличное
TTFB (Time to First Byte)БыстрыйМедленнее (сервер рендерит)Быстрый (CDN)
ИнтерактивностьПосле загрузки JSПосле гидратацииПосле гидратации
Серверная нагрузкаМинимальнаяВысокая (каждый запрос)Минимальная (при сборке)
Динамические данныеЛегкоЛегкоСложно
ПримерыCreate React AppNext.js (SSR режим)Next.js (SSG режим), Gatsby

Фреймворки с поддержкой SSR:

  • Next.js — самый популярный, поддерживает SSR, SSG, ISR, Server Components.
  • Nuxt.js — для Vue.
  • SvelteKit — для Svelte.
  • Remix — фокус на веб-стандартах и производительности.

Когда использовать SSR:

  • Страницы с динамическим контентом, который важен для SEO.
  • Когда важна скорость первого отображения контента.
  • Социальные сети, маркетплейсы, новостные сайты.

Когда CSR достаточно:

  • Админ-панели и внутренние инструменты (не нужен SEO).
  • Приложения с аутентификацией, где контент персонализирован.
  • Прототипы и MVP.

Кандидат отлично понимает тему и правильно использует термин «гидратация» — это показывает глубокое понимание процесса, а не поверхностное знание.

Вопрос 20. Что лучше использовать — Fragment или div для обёртки элементов и почему.

Таймкод: 00:30:15

Ответ собеседника: Правильный. Если нужна обёртка для стилей — использовать div. Если элементы могут существовать без дополнительной обёртки — лучше Fragment, чтобы не разрастать DOM-дерево. Чем больше DOM-дерево, тем ниже производительность.

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

Кандидат дала точный и практичный ответ. Это понимание на уровне эксперта — знает не только что использовать, но и почему.

Fragment в React

Fragment — это специальный компонент, который позволяет группировать несколько элементов без добавления дополнительного узла в DOM. Он решает проблему, что React-компонент должен возвращать один корневой элемент.

Синтаксис:

// Полный синтаксис
import { Fragment } from 'react';

function List() {
return (
<Fragment>
<h1>Title</h1>
<p>Description</p>
</Fragment>
);
}

// Краткий синтаксис (сахар)
function List() {
return (
<>
<h1>Title</h1>
<p>Description</p>
</>
);
}

Проблема, которую решает Fragment:

// Без Fragment — приходится оборачивать в div
function Columns() {
return (
<div> {/* Лишний div в DOM */}
<td>Column 1</td>
<td>Column 2</td>
</div>
);
}

// С Fragment — нет лишнего элемента
function Columns() {
return (
<>
<td>Column 1</td>
<td>Column 2</td>
</>
);
}

Когда использовать Fragment:

  • Несколько элементов должны быть возвращены из компонента без обёртки.
  • Рендеринг ячеек таблицы (<tr>, <td>), где лишний div нарушает семантику.
  • Списки элементов, где обёртка не нужна.
  • Условный рендеринг нескольких элементов.
// Таблица — Fragment необходим
function TableRow({ cells }) {
return (
<tr>
{cells.map(cell => (
<Fragment key={cell.id}>
<td>{cell.name}</td>
<td>{cell.value}</td>
</Fragment>
))}
</tr>
);
}

// Условный рендеринг
function Status({ isLoggedIn, isAdmin }) {
return (
<>
{isLoggedIn && <UserProfile />}
{isAdmin && <AdminPanel />}
<MainContent />
</>
);
}

Когда использовать div:

  • Нужна обёртка для применения стилей (flex, grid, padding, margin).
  • Нужен элемент для обработки событий (onClick, onMouseEnter).
  • Нужен элемент для атрибутов (className, id, data-атрибуты).
  • Семантически элемент должен быть контейнером.
// Нужны стили — div необходим
function Card({ children }) {
return (
<div className="card" style={{ padding: '16px', borderRadius: '8px' }}>
{children}
</div>
);
}

Ограничение краткого синтаксиса:

Краткий синтаксис <>...</> не поддерживает атрибуты, включая key. Если нужен ключ (например, в списке), используйте полный синтаксис:

// Краткий синтаксис — нельзя добавить key
<>
<ComponentA />
<ComponentB />
</>

// Полный синтаксис — можно добавить key
<Fragment key={item.id}>
<ComponentA />
<ComponentB />
</Fragment>

Влияние на производительность:

Каждый лишний div в DOM — это дополнительный узел, который браузер должен учитывать при расчёте стилей, layout и paint. В больших приложениях с тысячами компонентов разница между Fragment и div может быть заметной. Кроме того, лишние вложенные div усложняют отладку в DevTools и могут нарушать семантику HTML (особенно в таблицах и списках).

Кандидат правильно отметила, что размер DOM-дерева влияет на производительность — это понимание важного аспекта оптимизации фронтенд-приложений.

Вопрос 21. Что такое Higher-Order Components (HOC) — компоненты высшего порядка.

Таймкод: 00:31:24

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

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

Кандидат интуитивно понимает концепцию, но не смог чётко сформулировать назначение. Это нормально — HOC используется реже после появления хуков.

Что такое HOC

Higher-Order Component (HOC) — это функция, которая принимает компонент и возвращает новый компонент с дополнительной функциональностью. Это паттерн переиспользования логики, основанный на композиции.

Определение:

type HOC<P> = (Component: React.ComponentType<P>) => React.ComponentType<P>;

HOC — это не часть API React, а паттерн, вытекающий из природы React. Компоненты принимают пропсы и возвращают UI. HOC расширяет эту идею: функция принимает компонент и возвращает компонент.

Пример — добавление данных о текущем пользователе:

interface WithUserProps {
user: User | null;
}

// HOC, который добавляет пропс user к любому компоненту
function withUser<P extends WithUserProps>(
WrappedComponent: React.ComponentType<P>
) {
return function WithUserComponent(props: Omit<P, keyof WithUserProps>) {
const { user, isLoading } = useAuth();

if (isLoading) return <LoadingSpinner />;

return <WrappedComponent {...(props as P)} user={user} />;
};
}

// Использование
interface ProfilePageProps {
user: User | null;
title: string;
}

function ProfilePage({ user, title }: ProfilePageProps) {
return (
<div>
<h1>{title}</h1>
{user && <p>Hello, {user.name}</p>}
</div>
);
}

// Создаём компонент с добавленным user
const ProfilePageWithUser = withUser(ProfilePage);

// Использование — user передаётся автоматически
<ProfilePageWithUser title="My Profile" />

Пример — добавление логирования:

function withLogger<P extends object>(
WrappedComponent: React.ComponentType<P>,
componentName: string
) {
return function WithLoggerComponent(props: P) {
useEffect(() => {
console.log(`${componentName} mounted`);
return () => console.log(`${componentName} unmounted`);
}, []);

useEffect(() => {
console.log(`${componentName} updated`, props);
});

return <WrappedComponent {...props} />;
};
}

Пример — условный рендеринг по роли:

function withAdminCheck<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function AdminOnlyComponent(props: P) {
const { user } = useAuth();

if (!user || user.role !== 'admin') {
return <Redirect to="/login" />;
}

return <WrappedComponent {...props} />;
};
}

const AdminDashboard = withAdminCheck(Dashboard);

Проблемы HOC

Wrapper hell. При использовании нескольких HOC дерево компонентов становится глубоким и нечитаемым:

export default withRouter(
withAuth(
withTheme(
withLogger(MyComponent)
)
)
);

Конфликты пропсов. Несколько HOC могут передавать пропсы с одинаковыми именами, что приводит к неожиданному поведению.

Статические методы. HOC не переносят статические методы оборачиваемого компонента. Нужно использовать hoist-non-react-statics.

Отладка. В React DevTools видны все обёртки, что затрудняет отладку.

Современная альтернатива — кастомные хуки:

С появлением хуков HOC в значительной степени заменены кастомными хуками:

// Вместо HOC withUser
function useUser() {
const { user, isLoading } = useAuth();
return { user, isLoading };
}

// Использование — проще и понятнее
function ProfilePage({ title }: { title: string }) {
const { user, isLoading } = useUser();

if (isLoading) return <LoadingSpinner />;

return (
<div>
<h1>{title}</h1>
{user && <p>Hello, {user.name}</p>}
</div>
);
}

Когда HOC ещё используется:

  • В старых кодовых базах, написанных до появления хуков.
  • В библиотеках, где нужна обёртка компонента (например, react-redux раньше использовал HOC connect).
  • Когда нужно изменить сам компонент, а не только его поведение (редкий случай).

HOC — важный паттерн для понимания истории развития React, но в современной разработке кастомные хуки являются предпочтительным способом переиспользования логики.

Вопрос 22. Зачем нужны ключи (key) при рендеринге списка через map в React.

Таймкод: 00:32:56

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

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

Кандидат дала хороший ответ, объяснив назначение ключей. Для полноты картины стоит дополнить техническими деталями и примерами.

Зачем нужны ключи

Ключи (keys) помогают React идентифицировать элементы списка между рендерами. Когда список изменяется (добавление, удаление, переупорядочивание), React использует ключи для определения того, какой элемент соответствует какому.

Без ключей React использует индекс элемента в массиве как ключ по умолчанию. Это приводит к проблемам.

Проблемы при использовании индекса массива как ключа:

// Плохо: индекс как ключ
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}

Проблема 1: Неправильное состояние компонентов.

Представим список из трёх элементов. Пользователь вводит текст в первый элемент, затем удаляет его:

До удаления: После удаления:
[0] "Первый" (введён текст) [0] "Второй" (получает текст от "Первого")
[1] "Второй" [1] "Третий"
[2] "Третий"

React видит, что элемент с индексом 0 всё ещё существует, и не пересоздаёт компонент — он просто обновляет пропсы. В результате состояние ввода (текст) остаётся от предыдущего элемента.

Проблема 2: Неэффективные обновления.

При добавлении элемента в начало списка все индексы сдвигаются. React думает, что каждый элемент изменился, и обновляет все компоненты, хотя по факту добавился только один новый элемент.

Правильное использование ключей:

// Хорошо: стабильный уникальный идентификатор
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}

Требования к ключам:

  • Уникальность — ключ должен быть уникальным среди siblings (элементов одного уровня).
  • Стабильность — ключ не должен меняться между рендерами одного и того же элемента.
  • Предсказуемость — ключ должен однозначно идентифицировать элемент.

Чего нельзя использовать как ключ:

// Плохо: Math.random() — новый ключ при каждом рендере
<Item key={Math.random()} />

// Плохо: Date.now() — аналогичная проблема
<Item key={Date.now()} />

// Плохо: индекс массива при изменяемом списке
<Item key={index} />

Когда индекс как ключ допустим:

  • Список статический и никогда не изменяется (не добавляются, не удаляются, не переупорядочиваются элементы).
  • Элементы списка не имеют внутреннего состояния.
  • Список никогда не фильтруется.
// Допустимо: статический список без состояния
const NAV_ITEMS = ['Home', 'About', 'Contact'];

function Navigation() {
return (
<nav>
{NAV_ITEMS.map((item, index) => (
<a key={index} href={`/${item.toLowerCase()}`}>
{item}
</a>
))}
</nav>
);
}

Внутренняя механика:

React использует ключи в алгоритме reconciliation. При сравнении старого и нового виртуального DOM-деревьев React:

  1. Сопоставляет элементы по ключу.
  2. Если ключ найден в обоих деревьях — обновляет существующий элемент.
  3. Если ключ только в новом дереве — создаёт новый элемент.
  4. Если ключ только в старом дереве — удаляет элемент.

Это позволяет React минимизировать количество изменений в DOM и сохранять состояние компонентов корректно.

Кандидат правильно отметила, что без ключей может перерисовываться вся страница — это точное описание проблемы, хотя технически React перерисовывает не всю страницу, а все элементы списка, что тоже существенно влияет на производительность.