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

Собеседование Middle Frontend-разработчика + Live Coding | JS, Typescript, React, FSD, Next.js

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

Сегодня мы разберем интересное собеседование Middle Frontend-разработчика, прошедшее в формате live coding. Мы проанализируем вопросы, которые задавал интервьюер, оценим ответы кандидата и предложим развернутые, правильные ответы на каждый из них.

CSS и HTML

Вопрос 1: В чем преимущества использования CSS Modules по сравнению с обычным CSS?

Таймкод: 00:01:43

Ответ кандидата: Правильный. Кандидат верно отметил, что CSS Modules позволяют инкапсулировать стили компонентов благодаря добавлению хешей к классам.

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

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

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

Преимущества CSS Modules:

  • Изоляция стилей: Предотвращает конфликты имен классов и случайное влияние стилей одного компонента на другой.
  • Переиспользование стилей: Модульность позволяет легче переиспользовать стили и компоненты без опасения побочных эффектов.
  • Улучшенная организация кода: Стили компонента хранятся рядом с его JavaScript-кодом, что улучшает структуру проекта и облегчает навигацию.
  • Упрощение рефакторинга: Изменения в стилях одного компонента с меньшей вероятностью повлияют на другие части приложения.

Вопрос 2: CSS Modules добавляют хеши к классам, как это может повлиять на E2E тестирование, и как можно решить эту проблему?

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

Ответ кандидата: Неполный. Кандидат отметил, что это может быть проблемой для E2E тестов, но не предложил конкретных решений, кроме скриншот-тестов.

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

Действительно, динамически генерируемые хеши в именах классов CSS Modules могут создать сложности для E2E (end-to-end) тестирования, особенно при использовании селекторов CSS для поиска элементов. Тесты, основанные на жестко закодированных именах классов, станут нестабильными, так как хеши будут меняться при каждой сборке проекта.

Решения проблемы:

  • Data-атрибуты (Data attributes): Лучшим подходом является использование data-атрибутов для E2E тестирования. Вместо того чтобы полагаться на имена классов, разработчики могут добавлять специальные атрибуты, например data-testid="submit-button", к элементам, которые необходимо протестировать. Тестировщики затем могут использовать эти data-атрибуты для поиска элементов, что делает тесты более надежными и независимыми от изменений CSS Modules.
  • Консистентные имена классов для тестов: В некоторых случаях, для специфических E2E тестов, можно настроить CSS Modules таким образом, чтобы генерировать предсказуемые имена классов для определенных компонентов или элементов, например, через конфигурацию webpack или аналогичные инструменты. Однако этот подход менее гибкий и может усложнить поддержку.
  • Использование CSS Modules в тестовом окружении без хеширования: Можно настроить сборку проекта для тестового окружения таким образом, чтобы CSS Modules не добавляли хеши к именам классов, делая имена классов предсказуемыми для тестов. Но это может привести к расхождениям между тестовым и продакшн окружениями.
  • Page Object Model (POM): Применение паттерна Page Object Model в E2E тестах помогает абстрагировать селекторы элементов от логики тестов. В POM, селекторы хранятся в отдельных файлах, и при изменении имен классов CSS Modules, нужно будет обновить селекторы только в одном месте, а не во всех тестах.

Вопрос 3: Что такое специфичность в CSS, и сталкивались ли вы с проблемами специфичности при использовании CSS Modules?

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

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

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

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

Уровни специфичности (в порядке возрастания):

  1. Селекторы элементов (теги), например, p, div, span.
  2. Селекторы классов, например, .container, .button.
  3. Селекторы атрибутов, например, [type="text"].
  4. Псевдоклассы, например, :hover, :focus.
  5. Селекторы ID, например, #header, #navigation.
  6. Инлайновые стили, заданные непосредственно в HTML-атрибуте style.
  7. !important правило (самая высокая специфичность, следует избегать использования без крайней необходимости).

Проблемы специфичности в CSS Modules:

Несмотря на изоляцию стилей, специфичность остается важным аспектом даже при использовании CSS Modules. Проблемы могут возникнуть в следующих ситуациях:

  • Переопределение стилей: Когда необходимо переопределить стили компонента извне, например, при использовании компонентной библиотеки или при стилизации дочерних компонентов. Если внешние стили имеют недостаточную специфичность, они могут не переопределить стили, заданные в CSS Module.
  • Стилизация через глобальные стили: Иногда в проектах используются глобальные стили (например, для обнуления стилей, базовой типографики и т.д.), которые могут влиять на компоненты, стилизованные с помощью CSS Modules, из-за специфичности.
  • Использование !important в CSS Modules: Хотя CSS Modules минимизируют необходимость в !important, иногда разработчики могут прибегнуть к его использованию внутри модулей, что может усложнить переопределение этих стилей в дальнейшем.

Решения проблем специфичности:

  • Увеличение специфичности селекторов: При необходимости переопределить стили CSS Module, можно увеличить специфичность внешних селекторов, например, путем добавления более конкретных селекторов классов или использования селекторов атрибутов.
  • Использование композиции классов CSS Modules: CSS Modules позволяют композицию классов, что может быть использовано для переиспользования и расширения стилей, избегая необходимости в чрезмерном увеличении специфичности.
  • Внимательное проектирование архитектуры стилей: Тщательное планирование архитектуры стилей проекта, включая использование дизайн-систем и компонентных библиотек, помогает минимизировать проблемы, связанные со специфичностью.

Вопрос 4: Насколько важна доступность (accessibility) в лендингах и как вы проверяете доступность?

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

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

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

Доступность (Accessibility, A11y) - это практика создания веб-сайтов и веб-приложений, которые могут использоваться людьми с ограниченными возможностями. Это включает в себя людей с нарушениями зрения, слуха, моторики, когнитивными нарушениями и другими. Доступность также улучшает пользовательский опыт для всех, включая тех, кто использует мобильные устройства, медленное интернет-соединение или находится в шумной обстановке.

Важность доступности для лендингов:

  • Инклюзивность: Обеспечение доступности делает контент и услуги доступными для максимально широкой аудитории, включая людей с ограниченными возможностями, что является этически важным.
  • Расширение аудитории: Игнорирование доступности исключает значительную часть потенциальных пользователей и клиентов.
  • Юридические требования: Во многих странах существуют законы и стандарты, обязывающие обеспечивать доступность веб-сайтов (например, WCAG - Web Content Accessibility Guidelines).
  • SEO (Search Engine Optimization): Хотя прямая корреляция между доступностью и SEO ранжированием может быть не такой сильной, как считалось ранее, поисковые роботы отдают предпочтение семантически правильной и хорошо структурированной разметке, что является основой доступности. Доступные сайты, как правило, лучше индексируются и могут получать более высокий рейтинг.
  • Улучшенный пользовательский опыт (UX): Практики доступности, такие как логичная структура контента, четкая типографика и навигация с клавиатуры, улучшают UX для всех пользователей, не только для людей с ограниченными возможностями.

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

  • Ручное тестирование с использованием скринридеров (Screen Readers): Использование программ чтения с экрана, таких как NVDA, VoiceOver, JAWS, позволяет разработчикам и тестировщикам понять, как пользователи с нарушениями зрения воспринимают контент.
  • Навигация с клавиатуры: Проверка того, что все интерактивные элементы доступны и управляемы с клавиатуры (используя Tab, Shift+Tab, Enter, Spacebar и стрелки).
  • Инструменты автоматической проверки доступности: Существуют различные инструменты и расширения для браузеров (например, Axe DevTools, WAVE) которые автоматически сканируют веб-страницы на предмет распространенных проблем доступности и генерируют отчеты.
  • Линтеры и статические анализаторы кода: Интеграция линтеров доступности в процесс разработки (например, eslint-plugin-jsx-a11y для React) помогает выявлять проблемы доступности на ранних этапах.
  • Пользовательское тестирование с людьми с ограниченными возможностями: Наиболее эффективный способ проверки доступности - это проведение тестирования с участием реальных пользователей с различными видами инвалидности.
  • Аудит доступности: Профессиональные аудиторы доступности могут провести комплексную оценку веб-сайта и предоставить подробный отчет о проблемах и рекомендации по их устранению.

Вопрос 5: Что такое псевдоклассы и псевдоэлементы в CSS, и чем они отличаются? Приведите примеры псевдоэлементов.

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

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

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

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

Псевдоклассы:

  • Определение: Псевдоклассы используются для стилизации элементов в зависимости от их состояния или позиции в дереве DOM. Они начинаются с одинарного двоеточия (:).
  • Примеры:
    • :hover - стилизация элемента при наведении курсора мыши.
    • :focus - стилизация элемента, находящегося в фокусе (например, input при клике).
    • :active - стилизация элемента в момент нажатия (активации).
    • :first-child, :last-child, :nth-child(n) - стилизация первого, последнего или n-го дочернего элемента.
    • :disabled, :enabled - стилизация элементов в состоянии disabled или enabled.

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

  • Определение: Псевдоэлементы используются для стилизации виртуальных элементов, которые не существуют в DOM, но добавляются браузером для форматирования контента. Они начинаются с двойного двоеточия (::). (Хотя многие браузеры до сих пор поддерживают и одинарное двоеточие для псевдоэлементов для обратной совместимости).
  • Примеры:
    • ::before и ::after - создают виртуальные элементы, которые располагаются соответственно перед и после контента элемента. Часто используются для добавления декоративных элементов, иконок или дополнительного текста без изменения HTML.
    • ::first-line - стилизация первой строки текста в блочном элементе.
    • ::first-letter - стилизация первой буквы текста в блочном элементе.
    • ::selection - стилизация выделенного пользователем текста.
    • ::placeholder - стилизация текста placeholder в input и textarea элементах.

Ключевое отличие:

  • Псевдоклассы стилизуют существующие элементы на основе их состояния или позиции.
  • Псевдоэлементы создают виртуальные элементы внутри существующих элементов для стилизации или добавления контента.

Пример использования псевдоэлементов ::before и ::after:

.button::before {
content: ""; /* Обязательно для ::before и ::after */
display: inline-block;
width: 10px;
height: 10px;
background-color: red;
margin-right: 5px;
}

.button::after {
content: "→"; /* Добавляем стрелку после текста кнопки */
margin-left: 5px;
}

Этот CSS-код добавит красный квадрат перед текстом кнопки и стрелку после текста, используя псевдоэлементы ::before и ::after без изменения HTML-разметки.

JavaScript

Вопрос 6: Реализовать функцию delay(ms, value), которая возвращает промис, резолвящийся через ms миллисекунд со значением value.

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

Ответ кандидата: Правильный. Кандидат корректно реализовал функцию delay с использованием Promise и setTimeout.

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

function delay(ms, value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, ms);
});
}

Разъяснение:

Функция delay принимает два аргумента:

  • ms (milliseconds): Время задержки в миллисекундах.
  • value: Значение, которое промис должен вернуть после задержки.

Функция создает новый Promise. Внутри колбэк-функции промиса используется setTimeout, который откладывает выполнение кода на заданное количество миллисекунд (ms). После истечения времени задержки, функция resolve(value) вызывается, что переводит промис в состояние "resolved" (выполнен) и передает значение value в качестве результата.

Вопрос 7: Дан код с использованием функции delay и массивом значений. Какой будет порядок вывода в консоль и почему?

const values = [1, 2, 3];

async function main() {
console.log('Дан');
values.forEach(async (val) => {
const result = await delay(1000, val);
console.log(result);
});
console.log('Дан');
}

main();

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

Ответ кандидата: Неправильный. Кандидат предположил, что вывод будет "Дан 1 2 3 Дан" с задержкой в секунду между числами, но не учел асинхронную природу forEach и то, что forEach не ждет завершения асинхронных операций.

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

Порядок вывода в консоль будет следующим:

Дан
Дан
1
2
3

Объяснение:

  1. console.log('Дан'); (первый): Первый console.log('Дан'); выполняется синхронно и выводится в консоль сразу.
  2. values.forEach(...): Запускается цикл forEach. Важно понимать, что forEach не является асинхронным и не ждет завершения асинхронных операций внутри своего колбэка.
  3. Асинхронные операции в forEach: Для каждого элемента массива values, forEach вызывает асинхронную функцию async (val) => { ... }. Эти асинхронные функции начинают выполняться параллельно, но forEach не ждет их завершения.
  4. console.log('Дан'); (второй): Поскольку forEach не ждет асинхронных операций, цикл forEach быстро завершается, и второй console.log('Дан'); выполняется сразу после первого, до того, как промисы от delay зарезолвятся.
  5. await delay(...) и console.log(result);: Через секунду после запуска каждой асинхронной функции в forEach, delay промис резолвится, await дожидается этого разрешения и выполняется console.log(result);. Поскольку асинхронные функции в forEach были запущены почти одновременно, значения 1, 2, и 3 выводятся в консоль примерно через секунду после запуска main, но не гарантированно в порядке 1, 2, 3 из-за параллельного выполнения. Порядок может зависеть от различных факторов, но чаще всего вывод будет именно в порядке 1, 2, 3 из-за порядка итерации forEach.

Ключевой момент: forEach не подходит для последовательного выполнения асинхронных операций. Для последовательного выполнения асинхронных операций в цикле нужно использовать другие конструкции, такие как for...of или Promise.all в сочетании с map.

Вопрос 8: Как изменить код, чтобы гарантировать вывод в консоль Дан, затем 1, затем 2, затем 3 и в конце снова Дан, с задержкой в секунду между числами?

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

Ответ кандидата: Правильный (второй вариант). Кандидат сначала предложил использовать Promise.all, что вывело бы все значения сразу после задержки, а затем предложил использовать цикл for...of, что является правильным решением.

Правильный ответ (с использованием for...of):

const values = [1, 2, 3];

async function main() {
console.log('Дан');
for (const val of values) {
const result = await delay(1000, val);
console.log(result);
}
console.log('Дан');
}

main();

Объяснение:

Замена forEach на for...of решает проблему. Цикл for...of итерирует массив последовательно. Ключевым моментом является использование await внутри цикла for...of.

  1. console.log('Дан'); (первый): Выполняется синхронно.
  2. for (const val of values): Начинается последовательная итерация массива values.
  3. const result = await delay(1000, val);: Для каждой итерации цикла, await delay(1000, val); ожидает разрешения промиса delay перед переходом к следующей итерации. Таким образом, код приостанавливает выполнение на 1 секунду.
  4. console.log(result);: После задержки и разрешения промиса, значение result (то есть val) выводится в консоль.
  5. Цикл for...of повторяется для каждого элемента массива values последовательно, с задержкой в 1 секунду между каждой итерацией.
  6. console.log('Дан'); (второй): После завершения цикла for...of, выполняется второй console.log('Дан');.

Правильный ответ (с использованием reduce):

const values = [1, 2, 3];

async function main() {
console.log('Дан');
await values.reduce(async (promiseChain, val) => {
await promiseChain;
const result = await delay(1000, val);
console.log(result);
}, Promise.resolve());
console.log('Дан');
}

main();

Объяснение:

Этот вариант использует reduce для создания цепочки промисов.

  1. console.log('Дан'); (первый): Выполняется синхронно.
  2. values.reduce(...): Используется reduce для последовательного выполнения асинхронных операций.
  3. async (promiseChain, val) => { ... }: Колбэк-функция reduce является асинхронной. promiseChain аккумулирует промис из предыдущей итерации (или Promise.resolve() для первой итерации).
  4. await promiseChain;: Перед началом текущей асинхронной операции, await promiseChain; ожидает завершения промиса из предыдущей итерации, обеспечивая последовательное выполнение.
  5. const result = await delay(1000, val);: Выполняется асинхронная операция delay с задержкой в 1 секунду.
  6. console.log(result);: После задержки и разрешения промиса, значение result выводится в консоль.
  7. Promise.resolve() (initialValue): Promise.resolve() используется как начальное значение для reduce, чтобы запустить цепочку промисов.
  8. console.log('Дан'); (второй): После завершения цепочки промисов, выполняется второй console.log('Дан');.

Вопрос 9: Какой результат выведет следующий код?

const set = new Set([
{ value: 1 },
{ value: 2 },
{ value: 1 },
]);

console.log(Array.from(set).length);

Таймкод: 00:18:45

Ответ кандидата: Правильный. Кандидат верно сказал, что результатом будет 2, так как Set хранит только уникальные значения по ссылке, а объекты в массиве создаются с разными ссылками.

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

Результатом выполнения кода будет 2.

Объяснение:

  1. const set = new Set([...]): Создается новый объект Set. Set - это коллекция, которая хранит только уникальные значения.
  2. [{ value: 1 }, { value: 2 }, { value: 1 }]: В Set передается массив, содержащий три объекта. Важно понимать, что в JavaScript объекты сравниваются по ссылке, а не по значению.
  3. Уникальность объектов в Set: Даже несмотря на то, что первый и третий объекты имеют одинаковое свойство value: 1, это разные объекты в памяти с разными ссылками. Поэтому с точки зрения Set, они считаются разными.
  4. Array.from(set): Array.from(set) преобразует Set обратно в массив. В массиве будут только уникальные значения из Set. В данном случае, это будут два объекта: { value: 1 } (первый экземпляр) и { value: 2 }.
  5. .length: Свойство .length массива возвращает количество элементов в массиве. В массиве, полученном из Set, будет 2 элемента.
  6. console.log(...): В консоль выводится длина массива, которая равна 2.

Чтобы Set считал объекты с одинаковым value одинаковыми, нужно было бы использовать значения примитивных типов или сравнивать объекты по значению вручную перед добавлением в Set.

Вопрос 10: Какой результат выведет следующий асинхронный код?

async function a() {
return b();
}

async function b() {
return 1;
}

async function main() {
const result = await a();
console.log(result);
}

main();

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

Ответ кандидата: Правильный. Кандидат верно определил, что результатом будет 1, так как await дожидается разрешения промиса и возвращает зарезолвленное значение.

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

Результатом выполнения кода будет 1.

Объяснение:

  1. async function a() { return b(); }: Функция a объявлена как асинхронная (async). Она вызывает функцию b и возвращает результат ее вызова. Поскольку b также асинхронная функция, a возвращает промис, который зарезолвится в значение, возвращенное функцией b.
  2. async function b() { return 1; }: Функция b объявлена как асинхронная и возвращает примитивное значение 1. Когда асинхронная функция возвращает примитивное значение, JavaScript автоматически оборачивает его в зарезолвленный промис. То есть, return 1; эквивалентно return Promise.resolve(1);.
  3. async function main() { ... }: Функция main также асинхронная.
  4. const result = await a();: Внутри main, await a(); вызывает функцию a и ждет разрешения промиса, который возвращает a. Как мы выяснили, a возвращает промис, который резолвится в значение, возвращенное b, а b возвращает промис, который резолвится в 1.
  5. console.log(result);: После разрешения промиса, возвращенного a, значение 1 присваивается переменной result, и console.log(result); выводит 1 в консоль.

Ключевой момент: await "распаковывает" зарезолвленное значение из промиса. Цепочка асинхронных функций a и b выполняется последовательно, и в итоге, в result попадает зарезолвленное значение из самого последнего промиса в цепочке.

TypeScript

Вопрос 11: Приведите примеры утилитарных типов в TypeScript и объясните, зачем они нужны.

Таймкод: 00:20:39

Ответ кандидата: Правильный. Кандидат привел примеры утилитарных типов (Omit, Pick, Readonly, Partial) и верно объяснил их назначение - помощь в построении типов.

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

Утилитарные типы в TypeScript - это группа встроенных типов, которые предоставляют удобные способы трансформации существующих типов. Они помогают в решении общих задач при работе с типами, делая код более лаконичным и выразительным.

Зачем нужны утилитарные типы:

  • Переиспользование типов: Утилитарные типы позволяют создавать новые типы на основе уже существующих, избегая дублирования кода и повышая его переиспользуемость.
  • Уменьшение бойлерплейта: Они сокращают количество кода, необходимого для определения сложных типов.
  • Улучшение читаемости: Использование утилитарных типов делает код более читаемым и понятным, так как они явно выражают намерения разработчика по трансформации типов.
  • Поддержка DRY (Don't Repeat Yourself): Утилитарные типы способствуют принципу DRY, позволяя определить базовый тип и затем создавать его вариации с помощью утилит, вместо того, чтобы каждый раз определять типы с нуля.

Примеры утилитарных типов:

  • Partial<Type>: Делает все свойства типа Type опциональными.

    interface User {
    id: number;
    name: string;
    email: string;
    }

    type PartialUser = Partial<User>;

    const partialUser: PartialUser = { // Все свойства опциональны
    name: "John Doe",
    };
  • Required<Type>: Делает все свойства типа Type обязательными (обратно к Partial).

    type RequiredUser = Required<PartialUser>;

    // const requiredUser: RequiredUser = { // Ошибка: Свойства id и email отсутствуют
    // name: "John Doe",
    // };

    const requiredUser: RequiredUser = { // Все свойства обязательны
    id: 1,
    name: "John Doe",
    email: "john.doe@example.com",
    };
  • Readonly<Type>: Делает все свойства типа Type доступными только для чтения.

    type ReadonlyUser = Readonly<User>;

    const readonlyUser: ReadonlyUser = {
    id: 1,
    name: "John Doe",
    email: "john.doe@example.com",
    };

    // readonlyUser.name = "Jane Doe"; // Ошибка: Свойство 'name' доступно только для чтения.
  • Pick<Type, Keys>: Создает новый тип, выбирая только указанные свойства Keys из типа Type.

    type UserNameAndEmail = Pick<User, 'name' | 'email'>;

    const userDetails: UserNameAndEmail = {
    name: "John Doe",
    email: "john.doe@example.com",
    // id: 1, // Ошибка: Свойство 'id' отсутствует в типе 'UserNameAndEmail'.
    };
  • Omit<Type, Keys>: Создает новый тип, исключая указанные свойства Keys из типа Type.

    type UserWithoutId = Omit<User, 'id'>;

    const newUser: UserWithoutId = {
    name: "John Doe",
    email: "john.doe@example.com",
    // id: 1, // Ошибка: Свойство 'id' отсутствует в типе 'UserWithoutId'.
    };
  • Record<Keys, Type>: Создает тип объекта, где ключи - это типы из Keys (обычно объединение строковых или числовых литералов), а значения имеют тип Type.

    type StatusMap = Record<'pending' | 'inProgress' | 'completed', string>;

    const taskStatuses: StatusMap = {
    pending: "Ожидает выполнения",
    inProgress: "В процессе",
    completed: "Завершено",
    };
  • ReturnType<Type>: Извлекает тип возвращаемого значения функции Type.

    function greet(name: string): string {
    return `Hello, ${name}!`;
    }

    type GreetingReturnType = ReturnType<typeof greet>; // type GreetingReturnType = string
  • Parameters<Type>: Извлекает типы параметров функции Type в виде кортежа.

    type GreetParametersType = Parameters<typeof greet>; // type GreetParametersType = [name: string]
  • Extract<Type, Union>: Извлекает из типа Type типы, которые совместимы с типом Union.

    type AvailableTypes = string | number | boolean;
    type StringOrNumber = Extract<AvailableTypes, string | number>; // type StringOrNumber = string | number
  • Exclude<Type, ExcludedUnion>: Исключает из типа Type типы, которые совместимы с типом ExcludedUnion.

    type BooleanOnly = Exclude<AvailableTypes, string | number>; // type BooleanOnly = boolean

Вопрос 12: Что такое Declaration Merging в TypeScript?

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

Ответ кандидата: Не знает. Кандидат не знаком с термином Declaration Merging.

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

Declaration Merging (Слияние объявлений) в TypeScript - это механизм, позволяющий объединять объявления с одинаковым именем, созданные в разных частях кода. Это работает для интерфейсов, пространств имен (namespaces) и перечислений (enums).

Наиболее распространенный случай - слияние интерфейсов.

Слияние интерфейсов:

Если в вашем проекте объявлено несколько интерфейсов с одним и тем же именем, TypeScript автоматически объединит их объявления в одно общее объявление. Свойства из всех объявлений интерфейсов будут объединены в итоговый интерфейс.

Пример:

// file1.ts
interface MyInterface {
propertyA: string;
}

// file2.ts
interface MyInterface {
propertyB: number;
}

// combined.ts
// TypeScript автоматически "сливает" объявления интерфейсов MyInterface

let myObject: MyInterface = {
propertyA: "hello",
propertyB: 123,
};

// Теперь MyInterface имеет свойства propertyA и propertyB

Зачем нужно Declaration Merging:

  • Расширение сторонних типов: Позволяет расширять типы из сторонних библиотек или модулей, добавляя к ним новые свойства или методы без изменения исходного кода библиотек. Это особенно полезно для добавления специфических свойств к глобальным объектам или интерфейсам.
  • Модульность и организация кода: Declaration Merging позволяет разделить определение интерфейса на несколько файлов или модулей, улучшая организацию кода, особенно в больших проектах.
  • Декларативное расширение: Позволяет декларативно расширять интерфейсы, просто объявляя новый интерфейс с тем же именем в другом месте кода.
  • Расширение глобальных интерфейсов: Часто используется для расширения глобальных интерфейсов, таких как Window, Global, или интерфейсов из DOM API, добавляя к ним собственные свойства или методы, специфичные для вашего приложения.

Пример расширения глобального интерфейса Window:

// global.d.ts (файл деклараций)
interface Window {
myCustomProperty: string; // Добавляем новое свойство к интерфейсу Window
}

window.myCustomProperty = "Custom value"; // Теперь это допустимо в TypeScript
console.log(window.myCustomProperty);

Ограничения и предостережения:

  • Конфликты типов: Если объявления интерфейсов с одинаковым именем содержат свойства с одинаковыми именами, но разными типами, TypeScript выдаст ошибку компиляции.
  • Порядок объявлений: Порядок объявлений интерфейсов не важен, TypeScript объединяет их независимо от порядка.
  • Не работает для типов-псевдонимов (type aliases): Declaration Merging работает только для интерфейсов, пространств имен и перечислений, но не для типов-псевдонимов, объявленных с помощью type.

Вопрос 13: Что такое strict: true в tsconfig.json и зачем нужна эта настройка?

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

Ответ кандидата: Правильный. Кандидат верно сказал, что strict: true включает набор строгих проверок типов для улучшения качества кода.

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

В конфигурационном файле tsconfig.json TypeScript, опция "strict": true включает набор строгих проверок типов. Активация этой опции настоятельно рекомендуется для большинства TypeScript проектов, так как она помогает выявлять потенциальные ошибки на этапе компиляции и делает код более надежным и поддерживаемым.

Что включает в себя strict: true:

Опция "strict": true является "групповой" опцией и включает в себя следующие строгие проверки типов:

  • noImplicitAny: Запрещает неявно определять тип any. Когда TypeScript не может вывести тип переменной, параметра функции или свойства объекта, он по умолчанию присваивает им тип any. noImplicitAny: true заставляет TypeScript явно требовать указания типа в таких ситуациях, предотвращая случайное использование any и потерю преимуществ статической типизации.
  • noImplicitThis: Вызывает ошибку, если this используется в функциях, где его тип не может быть выведен однозначно. Это помогает избежать ошибок, связанных с неправильным контекстом this в JavaScript.
  • strictNullChecks: Включает строгую проверку на null и undefined. В обычном режиме, значения null и undefined могут быть присвоены переменным любого типа. strictNullChecks: true делает типы null и undefined более явными и требует явной проверки на null и undefined перед использованием значений, которые могут быть null или undefined. Это помогает предотвратить распространенные ошибки "Cannot read property 'x' of null/undefined".
  • strictFunctionTypes: Включает строгую проверку типов функций. Обеспечивает более строгую совместимость типов функций, особенно при передаче функций в качестве аргументов или присваивании функций переменным.
  • strictBindCallApply: Включает строгую проверку типов для методов bind, call и apply функций. Гарантирует, что аргументы, передаваемые этим методам, соответствуют типам параметров функции.
  • noPropertyAccessFromOptionalChain: Делает типы более строгими при использовании optional chaining (?.). Предотвращает случайный доступ к свойствам, которые могут быть undefined в optional chain.
  • useUnknownInCatchVariables: По умолчанию, тип переменной e в блоке catch (e) является any. useUnknownInCatchVariables: true изменяет тип e на unknown, что требует явной проверки и уточнения типа пойманной ошибки перед ее использованием.
  • forceConsistentCasingInFileNames: Требует согласованное использование регистра символов в именах файлов. Помогает избежать проблем с импортом модулей на разных операционных системах, где файловые системы могут быть case-insensitive или case-sensitive.

Зачем нужна настройка strict: true:

  • Повышение качества кода: Строгие проверки типов помогают выявлять ошибки на ранних этапах разработки (во время компиляции), а не во время выполнения, что снижает вероятность багов в продакшн.
  • Улучшение надежности и поддерживаемости: Код, написанный со строгими проверками типов, как правило, более надежен, предсказуем и легче поддерживается в долгосрочной перспективе.
  • Улучшение понимания типов: Строгие проверки типов заставляют разработчиков более внимательно относиться к типам данных и явно указывать их, что способствует лучшему пониманию типов и архитектуры приложения.
  • Подготовка к будущим изменениям: Код, написанный со строгими проверками типов, легче адаптировать к будущим изменениям в TypeScript и JavaScript, так как он более явно выражает типы и зависимости.

Вопрос 14: В чем разница между типами any и unknown в TypeScript?

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

Ответ кандидата: В целом правильный. Кандидат верно описал any как тип, отключающий проверки типов, и unknown как тип, требующий явного уточнения типа перед использованием значения.

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

any и unknown - это два специальных типа в TypeScript, предназначенные для работы со значениями, тип которых неизвестен на момент компиляции. Однако между ними есть ключевое различие в уровне безопасности и строгости типизации.

any:

  • Тип "отключения" проверок типов: any по сути отключает статическую проверку типов для переменной, которой присвоен тип any. TypeScript компилятор не выполняет никаких проверок типов для значений типа any.
  • "Окно" в динамический JavaScript: any позволяет TypeScript коду взаимодействовать с динамическим JavaScript-кодом, где типы могут быть неизвестны или меняться во время выполнения.
  • Гибкость, но и опасность: any предоставляет максимальную гибкость, позволяя выполнять любые операции со значением типа any без ошибок компиляции. Однако, это также означает потерю преимуществ статической типизации. Использование any в больших количествах может свести на нет пользу от TypeScript, так как ошибки, связанные с типами, могут проявиться только во время выполнения (runtime errors).
  • Неявный any (Implicit Any): В TypeScript, если не указать явно тип переменной, параметра функции или свойства объекта, и TypeScript не сможет вывести тип автоматически, ему будет присвоен неявный тип any (Implicit Any), если не включена опция "noImplicitAny": true в tsconfig.json.

unknown:

  • Тип "безопасного any": unknown представляет собой более типобезопасную альтернативу any. Как и any, unknown используется для представления значений, тип которых неизвестен. Но, в отличие от any, unknown требует явного уточнения типа (type narrowing) перед использованием значения.
  • Требует явной проверки и уточнения типа: Прежде чем выполнить какие-либо операции со значением типа unknown, TypeScript заставляет разработчика выполнить проверку типа (например, с помощью typeof, instanceof, пользовательских type guards) и сузить тип до более конкретного типа. Это гарантирует, что операции выполняются с типами, которые ожидаются.
  • Безопасность и контроль: unknown обеспечивает большую безопасность, так как предотвращает случайное выполнение операций над значениями неизвестного типа без предварительной проверки и обработки. Он заставляет разработчиков явно обрабатывать неопределенность типов, что делает код более надежным.
  • Для работы с внешними API и данными неизвестного формата: unknown идеально подходит для ситуаций, когда вы работаете с данными, полученными из внешних источников (например, API, пользовательский ввод), где тип данных заранее неизвестен.

Примеры:

let valueAny: any = "строка";
console.log(valueAny.toUpperCase()); // Допустимо, хотя valueAny может быть и не строкой (потеря контроля типов)

let valueUnknown: unknown = "строка";
// console.log(valueUnknown.toUpperCase()); // Ошибка: Object is of type 'unknown'. (нужно сужение типа)

if (typeof valueUnknown === "string") {
console.log(valueUnknown.toUpperCase()); // Допустимо, тип сужен до string внутри блока if
}

function processData(data: unknown) {
if (typeof data === 'number') {
console.log(data * 2); // Допустимо, тип сужен до number внутри блока if
} else if (typeof data === 'string') {
console.log(data.toUpperCase()); // Допустимо, тип сужен до string внутри блока else if
} else {
console.log("Неизвестный формат данных");
}
}

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

  • Использовать unknown по умолчанию, когда вы работаете со значениями, тип которых заранее неизвестен. unknown обеспечивает типобезопасность и заставляет явно обрабатывать неопределенность типов.
  • Использовать any только в крайних случаях, когда вам действительно необходимо отключить проверки типов для определенной части кода, например, при миграции с JavaScript на TypeScript или при работе со сторонними библиотеками, для которых нет точных TypeScript-типов. Старайтесь минимизировать использование any и заменять его на более строгие типы, такие как unknown, или конкретные типы, как только это возможно.

React и Next.js

Вопрос 15: Сталкивались ли вы с React.lazy и для чего он используется?

Таймкод: 00:28:39

Ответ кандидата: Не использовал, но общее понимание верное. Кандидат не использовал React.lazy, но правильно предположил, что это связано с отложенной загрузкой и декомпозицией приложения.

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

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

Зачем нужен React.lazy:

  • Уменьшение размера первоначального бандла: Приложения React могут расти и становиться большими, что приводит к увеличению размера JavaScript-бандла. Большой бандл замедляет первоначальную загрузку страницы, так как браузеру нужно загрузить и обработать весь JavaScript-код перед тем, как приложение станет интерактивным. React.lazy позволяет разбить приложение на более мелкие чанки (chunks) и загружать их по требованию.
  • Улучшение времени загрузки: Загружая только те компоненты, которые нужны для отображения первоначального экрана, React.lazy значительно уменьшает время первоначальной загрузки и делает приложение быстрее для пользователя.
  • Оптимизация производительности: Ленивая загрузка не только улучшает время загрузки, но и может повысить общую производительность приложения, особенно для больших и сложных приложений, так как браузеру не нужно обрабатывать и выполнять код для компонентов, которые в данный момент не используются.
  • Code Splitting (Разделение кода): React.lazy тесно связан с концепцией Code Splitting, которая заключается в разделении JavaScript-кода приложения на более мелкие фрагменты (chunks), которые можно загружать независимо и по мере необходимости. React.lazy является одним из инструментов для реализации Code Splitting в React.

Как использовать React.lazy:

React.lazy принимает функцию, которая должна динамически импортировать компонент. Динамический импорт (dynamic import) - это фича JavaScript (ES Modules), которая позволяет загружать модули асинхронно по запросу.

import React, { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent')); // Динамический импорт

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Загрузка...</div>}> {/* fallback - компонент для отображения во время загрузки */}
<LazyComponent /> {/* Лениво загружаемый компонент */}
</Suspense>
</div>
);
}

Suspense:

React.lazy должен быть обернут в компонент Suspense. Suspense позволяет "подвесить" рендеринг компонентов, которые еще не загрузились, и отобразить резервный контент (fallback) во время загрузки. Свойство fallback компонента Suspense принимает React-элемент, который будет отображаться, пока лениво загружаемый компонент загружается.

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

  • Для компонентов, которые не нужны при первоначальной загрузке: Например, компоненты, отображаемые на отдельных страницах, в модальных окнах, вкладках или разделах, которые пользователь не видит сразу.
  • Для больших и "тяжелых" компонентов: Компоненты, которые содержат много кода или имеют зависимости от больших библиотек, особенно подходят для ленивой загрузки, чтобы уменьшить размер первоначального бандла.
  • Для маршрутизации (Routing): Часто используется для ленивой загрузки компонентов, связанных с отдельными маршрутами в React-приложениях с маршрутизацией (например, с использованием React Router).

Преимущества React.lazy:

  • Простота использования: API React.lazy достаточно простой и легко интегрируется в существующие React-приложения.
  • Интеграция с Suspense: Suspense обеспечивает декларативный и удобный способ обработки состояния загрузки ленивых компонентов.
  • Улучшение UX: Более быстрая первоначальная загрузка и отзывчивость приложения улучшают пользовательский опыт.

Вопрос 16: Что такое React Suspense и с чем его можно использовать, кроме React.lazy?

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

Ответ кандидата: Неполный ответ, упомянул use, но не уверенно и без деталей. Кандидат упомянул use, но не смог четко связать Suspense с асинхронными источниками данных.

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

React Suspense - это механизм в React, позволяющий "подвешивать" рендеринг компонентов до тех пор, пока не будут выполнены определенные условия, такие как загрузка кода (с помощью React.lazy) или получение данных из асинхронного источника. Suspense предоставляет декларативный способ обработки состояний загрузки и ожидания в React-приложениях.

Первоначальное назначение Suspense (для Code Splitting с React.lazy):

Как было описано в предыдущем вопросе, Suspense изначально был представлен в React вместе с React.lazy для обработки состояния загрузки лениво загружаемых компонентов. Компонент Suspense оборачивает React.lazy, и пока ленивый компонент загружается, Suspense отображает fallback-компонент (резервный контент), заданный в свойстве fallback. После завершения загрузки ленивого компонента, Suspense автоматически переключается на рендеринг загруженного компонента.

Расширение возможностей Suspense с React Server Components и use Hook:

В новых версиях React (особенно в контексте React Server Components и React 18+), возможности Suspense были расширены. Теперь Suspense может использоваться не только для Code Splitting, но и для ожидания разрешения промисов, связанных с асинхронными источниками данных.

use Hook:

Ключевым API для работы с Suspense и асинхронными данными стал use Hook. use - это специальный Hook React, который позволяет "подвесить" рендеринг компонента, если внутри него вызывается промис, который еще не зарезолвился.

Как использовать Suspense с use Hook для асинхронных данных (в Server Components):

import React, { Suspense, use } from 'react';

async function fetchData() { // Асинхронная функция, возвращающая промис
const response = await fetch('/api/data');
return response.json();
}

function DataComponent() {
const dataPromise = fetchData(); // Запускаем асинхронный запрос и получаем промис
const data = use(dataPromise); // Вызываем use Hook с промисом
// Suspense "подвесит" рендеринг DataComponent, пока dataPromise не зарезолвится
return (
<div>
{/* Отображаем данные после разрешения промиса */}
{data.map(item => <div key={item.id}>{item.name}</div>)}
</div>
);
}

function MyPage() {
return (
<Suspense fallback={<div>Загрузка данных...</div>}> {/* fallback для DataComponent */}
<DataComponent /> {/* Компонент, который "подвешивает" рендеринг при ожидании данных */}
</Suspense>
);
}

Объяснение:

  1. fetchData(): Асинхронная функция, которая отправляет запрос к API и возвращает промис, резолвящийся с полученными данными.
  2. DataComponent:
    • const dataPromise = fetchData();: Вызывает fetchData() и получает промис, представляющий асинхронный запрос данных.
    • const data = use(dataPromise);: Вызывает use(dataPromise). Если dataPromise еще не зарезолвился, use "подвешивает" рендеринг DataComponent и "бросает" промис вверх по дереву компонентов.
    • Suspense перехватывает "брошенный" промис: Компонент Suspense, оборачивающий DataComponent в MyPage, перехватывает этот "брошенный" промис. Пока промис не зарезолвится, Suspense отображает fallback-компонент (
      Загрузка данных...
      ).
    • После разрешения промиса: Когда dataPromise резолвится, React повторно пытается отрендерить DataComponent. На этот раз, use(dataPromise) вернет зарезолвленное значение (data), и рендеринг продолжится, отображая данные.
  3. MyPage: Оборачивает DataComponent в Suspense для обработки состояния загрузки данных.

Ключевые моменты:

  • use Hook - только в Server Components: use Hook может быть вызван только внутри React Server Components (не в Client Components).
  • Suspense - граница обработки состояния загрузки: Suspense определяет границу, в пределах которой обрабатывается состояние загрузки. Любой компонент, "подвешивающий" рендеринг (через use или React.lazy), должен быть обернут в Suspense.
  • Декларативный подход к асинхронности: Suspense и use предоставляют более декларативный и удобный способ работы с асинхронными операциями в React, по сравнению с традиционными подходами с использованием useEffect и состояний.
  • Улучшенный UX для асинхронных операций: Suspense позволяет отображать fallback-контент во время загрузки данных, обеспечивая более плавный и предсказуемый пользовательский опыт при работе с асинхронными операциями.

Вопрос 17: В чем отличие Server Components от Client Components в React 18+?

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

Ответ кандидата: Неполный ответ, с некоторыми неточностями. Кандидат описал, что Server Components рендерятся на сервере, а Client Components на клиенте, упомянул об отсутствии обработчиков событий в Server Components и возможность доступа к базе данных. Но не совсем точно сказал, что Client Components "не рендерятся на сервере".

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

React Server Components (RSC) и Client Components - это два фундаментально разных типа компонентов в React 18+ (и особенно в Next.js App Router), которые рендерятся на разных средах (сервере и клиенте) и имеют разные возможности и ограничения. Понимание различий между ними критически важно для эффективной разработки современных React-приложений.

React Server Components (RSC):

  • Рендеринг только на сервере: Server Components рендерятся исключительно на сервере, во время сборки приложения или по запросу пользователя (request-time rendering). Код Server Components не попадает в браузер.
  • Доступ к бэкенд-ресурсам: Server Components имеют прямой доступ к бэкенд-ресурсам, таким как базы данных, файловая система, API без необходимости создавать API endpoints. Это устраняет "waterfall" запросов к API со стороны клиента и упрощает получение данных.
  • Не содержат интерактивности: Server Components не могут использовать React Hooks (такие как useState, useEffect, onClick и т.д.) и, следовательно, не поддерживают интерактивность на стороне клиента. Они предназначены для отображения статического или динамического контента, получаемого с сервера.
  • Оптимизация производительности:
    • Меньший размер JavaScript-бандла: Так как код Server Components не отправляется в браузер, размер JavaScript-бандла на клиенте значительно уменьшается, что ускоряет первоначальную загрузку страницы.
    • Ускоренный Time to First Byte (TTFB): Серверный рендеринг позволяет генерировать HTML на сервере и отправлять его браузеру быстрее, улучшая TTFB.
    • Потоковая передача HTML (Streaming HTML): React 18+ поддерживает потоковую передачу HTML с Server Components, что позволяет браузеру начать отображение контента быстрее, даже если данные или другие части страницы еще загружаются.
  • Безопасность: Код Server Components выполняется только на сервере, что повышает безопасность, так как конфиденциальный код (например, ключи API, код доступа к базе данных) не раскрывается на клиенте.
  • Кэширование результатов: Результаты рендеринга Server Components могут быть эффективно кэшированы на сервере, уменьшая нагрузку на бэкенд и ускоряя время ответа.

React Client Components:

  • Рендеринг на клиенте и сервере (SSR/CSR): Client Components рендерятся как на сервере (для первоначальной загрузки HTML), так и на клиенте (для интерактивности и обновлений UI). Код Client Components отправляется в браузер в составе JavaScript-бандла.
  • Интерактивность: Client Components поддерживают интерактивность, они могут использовать React Hooks (например, для управления состоянием, эффектами, обработчиками событий). Они предназначены для создания динамичных и интерактивных UI.
  • Ограниченный доступ к бэкенд-ресурсам: Client Components не имеют прямого доступа к бэкенд-ресурсам. Для получения данных с сервера, они должны использовать клиентские запросы к API endpoints.
  • Гидратация (Hydration): После первоначального серверного рендеринга Client Components, React выполняет процесс гидратации на клиенте. Во время гидратации, React "оживляет" статичный HTML, полученный с сервера, привязывая к нему JavaScript-логику, обработчики событий и восстанавливая состояние компонентов. Гидратация "стоит" производительности на клиенте и может замедлить Time to Interactive (TTI).

Ключевые различия в таблице:

FeatureServer Components (RSC)Client Components
РендерингТолько на сервереСервер (SSR) и клиент (CSR)
Где выполняется кодСерверБраузер
ИнтерактивностьНет (не используют Hooks)Да (используют Hooks)
Доступ к бэкендуПрямой доступ к бэкенд-ресурсамЧерез API endpoints
Размер бандлаМеньшеБольше
Первоначальная загрузкаБыстрееМедленнее
БезопасностьВышеНиже
КэшированиеЭффективное кэширование на сервереМенее эффективное кэширование
ГидратацияНетДа (требует производительности)

Когда использовать Server Components vs Client Components:

  • Server Components - для большинства случаев: По умолчанию, старайтесь использовать Server Components везде, где это возможно. Они идеально подходят для:
    • Отображения контента (текст, изображения, данные из базы данных).
    • Получения и обработки данных на сервере.
    • Страниц просмотра (product listings, блоги, документация).
    • Компонентов, не требующих интерактивности.
  • Client Components - только для интерактивности: Используйте Client Components только тогда, когда необходима интерактивность на стороне клиента, например:
    • Интерактивные формы.
    • Кнопки, переключатели, слайдеры.
    • Компоненты, управляющие состоянием на клиенте (например, корзина покупок, фильтры).
    • Анимации и переходы.
    • Компоненты, использующие браузерные API (например, геолокация, Web Storage).

Совместное использование Server Components и Client Components:

В React-приложении можно комбинировать Server Components и Client Components. Server Components могут рендерить Client Components в качестве дочерних элементов. Обратное неверно: Client Components не могут импортировать и рендерить Server Components напрямую.

Маркировка Client Components:

Чтобы React знал, что компонент должен быть отрендерен как Client Component, необходимо явно пометить его, добавив директиву 'use client' в начале файла компонента.

// MyClientComponent.tsx
'use client'; // Маркируем компонент как Client Component

import React, { useState } from 'react';

function MyClientComponent() {
const [count, setCount] = useState(0);
return (
<div>
{/* Интерактивный компонент */}
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
</div>
);
}

export default MyClientComponent;

Вопрос 18: Дан код React-компонента с useState и useEffect. Какие проблемы вы видите в этом коде и как их исправить?

import React, { useState, useEffect } from 'react';

function CounterComponent() {
const [count, setCount] = useState(0);

useEffect(() => {
if (count === 5) {
setCount(0);
}
console.log('Count updated:', count);
}, [count]);

const increment = () => {
setCount(count + 1);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

Таймкод: 00:34:58

Ответ кандидата: Правильно определил проблему бесконечного цикла. Кандидат верно указал на проблему бесконечного цикла ререндеров из-за setCount в useEffect и предложил убрать useEffect или вынести условие в setCount.

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

Проблема:

Код содержит бесконечный цикл ререндеров (infinite re-render loop).

Объяснение бесконечного цикла:

  1. Инициализация: Компонент CounterComponent рендерится впервые. count инициализируется значением 0.
  2. Первый клик на "Increment":
    • Функция increment вызывается, setCount(count + 1) обновляет состояние count до 1.
    • React запускает процесс ререндеринга компонента.
  3. Ререндеринг и useEffect:
    • Компонент рендерится с новым значением count = 1.
    • useEffect вызывается, так как count (зависимость useEffect) изменился.
    • Условие if (count === 5) не выполняется, так как count равен 1.
    • console.log('Count updated:', count); выводит "Count updated: 1" в консоль.
  4. Второй, третий, четвертый клики: Аналогично, при каждом клике count увеличивается, компонент ререндерится, useEffect вызывается, условие if (count === 5) не выполняется, console.log срабатывает.
  5. Пятый клик на "Increment":
    • Функция increment вызывается, setCount(count + 1) обновляет состояние count до 5.
    • React запускает ререндеринг.
  6. Ререндеринг и useEffect (бесконечный цикл начинается):
    • Компонент рендерится с новым значением count = 5.
    • useEffect вызывается, так как count (зависимость useEffect) изменился.
    • Условие if (count === 5) выполняется, так как count равен 5.
    • setCount(0); вызывается внутри useEffect, что снова обновляет состояние count до 0.
    • Это обновление состояния count снова запускает ререндеринг компонента.
    • Цикл начинается снова с пункта 3: компонент рендерится, useEffect вызывается, и так далее бесконечно.

Исправление (Вариант 1 - Убрать useEffect):

В данном конкретном примере, useEffect не нужен, так как логика сброса счетчика до 0 при достижении 5 может быть непосредственно перенесена в функцию increment:

import React, { useState } from 'react'; // useEffect больше не нужен

function CounterComponent() {
const [count, setCount] = useState(0);

const increment = () => {
setCount(prevCount => { // Используем функциональное обновление состояния
const newCount = prevCount + 1;
return newCount === 5 ? 0 : newCount; // Сброс до 0 при достижении 5
});
};

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

Объяснение исправления (Вариант 1):

  • Условие if (newCount === 5) теперь проверяется внутри функции increment перед обновлением состояния.
  • Если newCount становится равным 5, setCount(0) сбрасывает счетчик до 0.
  • Иначе, setCount(newCount) увеличивает счетчик на 1.
  • useEffect удален, так как логика сброса счетчика теперь выполняется синхронно в обработчике события onClick.

Исправление (Вариант 2 - Использовать useEffect с правильной зависимостью и условием):

Если по какой-то причине логику сброса счетчика нужно оставить в useEffect (например, если это часть более сложной логики, зависящей от побочных эффектов), то можно исправить код, изменив условие в useEffect и добавив дополнительную зависимость, чтобы предотвратить бесконечный цикл:

import React, { useState, useEffect, useRef } from 'react';

function CounterComponent() {
const [count, setCount] = useState(0);
const isResettingRef = useRef(false); // Ref для отслеживания сброса состояния

useEffect(() => {
if (count === 5 && !isResettingRef.current) { // Добавляем проверку ref и условие count === 5
isResettingRef.current = true; // Устанавливаем ref в true перед сбросом
setCount(0);
}
console.log('Count updated:', count);
}, [count]);

useEffect(() => { // Второй useEffect для сброса ref после ререндеринга
isResettingRef.current = false; // Сбрасываем ref после ререндеринга
}, [count]); // Зависимость count нужна, чтобы ref сбрасывался при каждом изменении count

const increment = () => {
setCount(count + 1);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

Объяснение исправления (Вариант 2):

  • isResettingRef = useRef(false);: Используется useRef для создания ref isResettingRef, который будет отслеживать, находится ли компонент в процессе сброса счетчика.
  • useEffect (первый):
    • if (count === 5 && !isResettingRef.current): Условие изменено. Теперь сброс счетчика происходит только если count === 5 и isResettingRef.current is false. Это предотвращает повторный сброс счетчика при каждом ререндере после установки count в 0.
    • isResettingRef.current = true;: Перед вызовом setCount(0), isResettingRef.current устанавливается в true.
    • setCount(0);: Сбрасывает счетчик.
  • useEffect (второй):
    • isResettingRef.current = false;: После каждого ререндеринга, второй useEffect сбрасывает isResettingRef.current обратно в false. Это позволяет сбросить счетчик снова при следующем достижении count === 5.
  • Зависимость [count] в обоих useEffect: useEffect вызываются только при изменении count, как и в исходном коде.

Какой вариант исправления выбрать?

  • Вариант 1 (убрать useEffect и перенести логику в increment) - предпочтительнее и проще для данного примера. Он устраняет необходимость в useEffect и делает код более понятным и эффективным.
  • Вариант 2 (использовать useEffect с useRef) - более сложный и менее читаемый для этого примера, но он демонстрирует, как можно использовать useEffect и useRef для предотвращения бесконечных циклов ререндеров в более сложных сценариях, где логика может быть действительно зависеть от побочных эффектов.

Вопрос 19: Дан код React-компонента для отображения списка элементов. Какие проблемы вы видите в этом коде и как их исправить?

import React, { useState } from 'react';

function ListComponent() {
const [listItems, setListItems] = useState([
{id:1, name: 'Item 1},
{id:2, name: 'Item 2},
{id:3, name: 'Item 3}
]);

const addItem = () => {
const newItem = { id: 4, text: 'New Item' };
setListItems([...listItems, newItem]);
};

return (
<div>
{listItems.map((item, index) => (
<div>
<p>{item.text}</p>
</div>
))}
<button onClick={addItem}>Add Item</button>
</div>
);
}

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

Ответ кандидата: Правильно определил проблему отсутствия key и использования индекса в качестве key. Кандидат верно указал на отсутствие key prop у <li> элементов и на то, что использование индекса в качестве key - это антипаттерн.

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

Проблема:

Основная проблема в коде - это использование индекса массива в качестве key prop при рендеринге списка элементов. Также, не идеально, что div обертка не имеет key.

Объяснение проблемы с использованием индекса в качестве key:

В React, key prop используется для идентификации элементов списка при рендеринге. React использует key для оптимизации процесса обновления списка, позволяя эффективно добавлять, удалять и переупорядочивать элементы.

Когда использование индекса в качестве key - это антипаттерн:

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

Проблемы, возникающие при использовании индекса в качестве key, когда порядок элементов меняется:

  • Неправильное обновление компонентов: React может неправильно определить, какие элементы списка изменились, были добавлены или удалены. Это может привести к неожиданному поведению, такому как:
    • Неправильное состояние компонентов: Состояние компонентов списка может быть потеряно или перепутано при изменении порядка элементов.
    • Проблемы с фокусом ввода: Фокус ввода в элементах списка может смещаться или теряться при изменении порядка.
    • Проблемы с анимациями и переходами: Анимации и переходы при добавлении, удалении или переупорядочивании элементов списка могут работать некорректно.
  • Снижение производительности: В некоторых случаях, неправильное обновление компонентов может привести к ненужным ререндерингам и снижению производительности.

Почему индекс не является стабильным идентификатором:

Индекс элемента массива зависит от его позиции в массиве. Когда порядок элементов в массиве меняется, индексы элементов также меняются. Таким образом, индекс не является стабильным и уникальным идентификатором для элемента списка.

Когда можно использовать индекс в качестве key (ограниченные случаи):

Использование индекса в качестве key может быть приемлемо только в очень ограниченных случаях, когда гарантированно, что:

  1. Список является статичным и никогда не меняется после первоначального рендеринга.
  2. Порядок элементов в списке никогда не будет изменяться (добавление, удаление, сортировка, фильтрация не происходят).

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

Исправление (Использовать уникальный id в качестве key):

Лучшим решением является использование стабильного и уникального идентификатора (например, id) для каждого элемента списка в качестве key prop.

import React, { useState } from 'react';

function ListComponent({ initialItems }) { // Переименовали prop на initialItems
const [listItems, setListItems] = useState(initialItems); // Используем initialItems для initial state

const addItem = () => {
const newItem = { id: Math.random().toString(), text: 'New Item' }; // Генерируем уникальный id (toString для key prop)
setListItems(prevListItems => [...prevListItems, newItem]); // Функциональное обновление состояния
};

return (
<div>
<ul>
{listItems.map((item) => ( // Убрали index из map
<li key={item.id}>{item.text}</li> // Используем item.id в качестве key
))}
</ul>
<button onClick={addItem}>Add Item</button>
</div>
);
}

Объяснение исправления:

  1. initialItems prop: Prop компонента переименован в initialItems для ясности и чтобы подчеркнуть, что это начальный список элементов.
  2. useState(initialItems): initialItems используется для инициализации состояния listItems.
  3. newItem.id = Math.random().toString(): При добавлении нового элемента, генерируется уникальный id с помощью Math.random().toString(). toString() преобразует число в строку, так как key prop ожидает строковое значение. В реальных приложениях, лучше использовать более надежные способы генерации уникальных ID, например, UUID библиотеки.
  4. key={item.id}: Теперь в <li> элементах используется item.id в качестве key prop. Это обеспечивает React стабильным и уникальным идентификатором для каждого элемента списка.
  5. Функциональное обновление состояния setListItems: В addItem используется функциональное обновление состояния setListItems(prevListItems => [...prevListItems, newItem]);. Это лучшая практика для обновления состояния, основанного на предыдущем состоянии, особенно при работе с асинхронными обновлениями состояния или при использовании strict mode React.

Дополнительные улучшения (не связанные с key, но рекомендуется):

  • Использовать более надежный способ генерации ID: Math.random() хоть и генерирует случайные числа, но не гарантирует абсолютную уникальность, особенно при большом количестве элементов или в сложных сценариях. Для production-приложений, рекомендуется использовать UUID (Universally Unique Identifier) библиотеки, такие как uuid или nanoid, для генерации гарантированно уникальных ID.
  • Проверка наличия key prop линтером: Настроить линтер (например, eslint-plugin-react-hooks) для предупреждения или ошибки, если key prop отсутствует при рендеринге списков. Это поможет предотвратить случайное пропущение key prop и связанные с этим проблемы.

Вопрос 20: Как бы вы сгенерировали уникальные ID для элементов списка в Server Components, чтобы избежать проблем гидратации, если Math.random() не подходит?

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

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

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

Для генерации уникальных ID в React Server Components, которые будут консистентны как на сервере, так и на клиенте, и избежать проблем гидратации, лучшим подходом является использование UUID (Universally Unique Identifier) библиотеки.

Почему Math.random() не подходит для Server Components и гидратации:

  • Недетерминированность на сервере и клиенте: Math.random() генерирует случайные числа, которые не будут одинаковыми при серверном рендеринге и последующей гидратации на клиенте. Если вы генерируете ID с помощью Math.random() на сервере, а затем пытаетесь использовать эти ID на клиенте во время гидратации, ID будут не совпадать, что приведет к mismatch при гидратации и ошибкам.
  • Проблемы с key prop и обновлением списка: Несоответствие ID между сервером и клиентом нарушит механизм key prop в React, так как React не сможет правильно сопоставить элементы списка, отрендеренные на сервере, с элементами на клиенте. Это может привести к тем же проблемам, что и использование индекса в качестве key (неправильное обновление компонентов, потеря состояния, проблемы с фокусом и анимациями).

Решение - UUID (Universally Unique Identifier):

UUID - это стандарт для генерации глобально уникальных 128-битных идентификаторов. UUID генерируются с использованием алгоритмов, которые гарантируют практически нулевую вероятность коллизии (совпадения) ID.

Преимущества использования UUID для генерации ID в Server Components:

  • Генерация на сервере: UUID можно генерировать на сервере в Server Components перед рендерингом, и эти UUID будут включены в HTML, отправленный браузеру.
  • Консистентность на сервере и клиенте: Поскольку UUID генерируются детерминированно (или псевдо-детерминированно, в зависимости от библиотеки), UUID, сгенерированные на сервере, будут консистентны и распознаны React на клиенте во время гидратации. Это избегает проблем mismatch гидратации и обеспечивает правильную работу key prop.
  • Глобальная уникальность: UUID гарантируют глобальную уникальность, что важно для больших приложений и распределенных систем, где нужно обеспечить уникальность идентификаторов в разных частях приложения и между разными экземплярами приложения.

Как использовать UUID в React Server Components (пример с библиотекой uuid):

  1. Установите UUID библиотеку:

    npm install uuid
  2. Импортируйте UUID библиотеку в Server Component:

    import { v4 as uuidv4 } from 'uuid'; // Импорт v4 (случайный UUID)

    function MyServerComponent({ initialItems }) {
    const [listItems, setListItems] = useState(initialItems);

    const addItem = () => {
    const newItem = { id: uuidv4(), text: 'New Item' }; // Генерируем UUID v4
    setListItems(prevListItems => [...prevListItems, newItem]);
    };

    return (
    <div>
    <ul>
    {listItems.map((item) => (
    <li key={item.id}>{item.text}</li>
    ))}
    </ul>
    <button onClick={addItem}>Add Item</button>
    </div>
    );
    }

Объяснение:

  • import { v4 as uuidv4 } from 'uuid';: Импортируем функцию v4 из библиотеки uuid. v4 генерирует случайные UUID (version 4 UUID). Есть и другие версии UUID, но v4 - наиболее часто используемая.
  • newItem.id = uuidv4();: В функции addItem, для каждого нового элемента генерируется UUID v4 с помощью uuidv4() и присваивается свойству id нового элемента.

Важно:

  • Генерация UUID на сервере: Убедитесь, что UUID генерируются на сервере в Server Components перед рендерингом HTML.
  • Передача UUID в Client Components: Если вам нужно передать UUID в Client Components (например, если Client Component обрабатывает добавление новых элементов и требует генерации ID на клиенте), передайте функцию генерации UUID (например, uuidv4) из Server Component в Client Component в качестве prop. Не импортируйте UUID библиотеку непосредственно в Client Components, если это возможно, чтобы минимизировать размер бандла Client Component.

Преимущества использования UUID:

  • Стабильные и уникальные ID: UUID обеспечивают стабильные и глобально уникальные идентификаторы.
  • Решение проблем гидратации: UUID, сгенерированные на сервере, обеспечивают консистентность ID на сервере и клиенте, решая проблемы mismatch гидратации.
  • Лучшая практика для Server Components: Использование UUID - это лучшая практика для генерации ID в React Server Components и для приложений, где важна консистентность ID между сервером и клиентом.

Архитектура и FSD

Вопрос 21: Расскажите о принципах FSD (Frontend Specific Design) архитектуры, которую вы используете.

Таймкод: 00:39:06

Ответ кандидата: В целом верное понимание FSD. Кандидат описал слои FSD, разделение на сущности внутри слоев, и экспорт через public API.

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

FSD (Frontend Specific Design) или Feature-Sliced Design - это архитектурный подход к организации frontend-проектов, направленный на улучшение масштабируемости, поддерживаемости и гибкости кодовой базы. FSD фокусируется на разделении приложения на независимые "слайсы" (slices) или "фичи" (features) и строгом соблюдении принципов инкапсуляции и разделения ответственности.

Ключевые принципы FSD:

  1. Разделение на слайсы (Slices / Features):

    • Приложение разделяется на независимые "слайсы" или "фичи", каждый из которых представляет собой отдельную бизнес-функциональность или доменную область приложения.
    • Примеры слайсов: user-profile, article-comments, product-catalog, authentication, settings.
    • Независимость и изоляция: Слайсы должны быть максимально независимы друг от друга. Изменения в одном слайсе должны минимально влиять на другие слайсы.
    • Слабая связанность (Loose Coupling): Связи между слайсами должны быть минимальными и явно определенными (например, через public API).
    • Переиспользуемость: Слайсы могут быть переиспользованы в разных частях приложения или даже в разных проектах.
  2. Слоистая архитектура (Layered Architecture):

    • Каждый слайс и все приложение в целом организуются по слоистой архитектуре. FSD выделяет несколько основных слоев:

      • entities (Сущности / Domain Entities): Содержит доменные сущности, бизнес-логику и типы, общие для всего приложения. Сущности представляют собой абстракции из предметной области (например, user, product, article, comment). Не зависят ни от каких других слоев внутри слайса или других слайсов (максимальная переиспользуемость).
      • features (Фичи / User Stories): Содержит UI-компоненты и логику, реализующие конкретные пользовательские сценарии или фичи. Компоненты из features используют сущности (entities) для отображения и взаимодействия с данными. Могут зависеть от сущностей своего слайса и сущностей из shared слоя.
      • widgets (Виджеты / UI Blocks): Содержит композитные UI-блоки, объединяющие несколько компонентов из features и shared для формирования более крупных UI-секций. Виджеты представляют собой готовые UI-блоки для использования на страницах. Могут зависеть от компонентов из features и shared слоя.
      • pages (Страницы / Application Views): Содержит компоненты, представляющие отдельные страницы или виды приложения. Страницы компонуют виджеты (widgets) и могут напрямую использовать компоненты из features и shared слоев для формирования UI страниц. Страницы являются "точкой входа" для пользователей в приложение.
      • app (Приложение / Application-level): Содержит глобальные настройки приложения, стили, маршрутизацию, хранилище состояния (store), и другие application-level сущности. Слой app является корневым слоем приложения. Может зависеть от всех других слоев, но сам не должен зависеть от других слайсов (только от shared).
      • shared (Общий / Shared Utilities): Содержит переиспользуемые утилиты, компоненты, стили, типы и функции, общие для всего приложения и всех слайсов. shared слой предназначен для разделения общих ресурсов и минимизации дублирования кода. Не должен зависеть ни от каких других слоев приложения, включая слайсы (максимальная переиспользуемость).
  3. Public API (Публичный API):

    • Каждый слайс и каждый слой должен иметь явно определенный Public API, который определяет, что именно можно использовать из этого слайса или слоя снаружи.
    • Инкапсуляция: Все внутренние детали реализации слайса или слоя должны быть скрыты за Public API (инкапсулированы). Это позволяет изменять внутреннюю реализацию без нарушения работы других частей приложения, которые используют Public API.
    • Управляемые зависимости: Использование Public API позволяет контролировать и ограничивать зависимости между слайсами и слоями. Зависимости должны быть направлены только через Public API.
    • index.ts (или index.js) file: Public API обычно реализуется через файл index.ts (или index.js) в корне каждого слоя или слайса, который экспортирует только те сущности, которые предназначены для публичного использования.
  4. Принцип "Импорт только снизу вверх" (Import "Down-to-Up"):

    • Слои более высокого уровня (например, pages, widgets, features) могут импортировать сущности из слоев более низкого уровня (например, entities, shared).
    • Слои более низкого уровня (например, entities, shared) не должны импортировать сущности из слоев более высокого уровня.
    • Нарушение этого принципа приводит к циклической зависимости и нарушению инкапсуляции.
    • Исключение: Слой app может зависеть от всех других слоев, но не должен зависеть от других слайсов (только от shared).

Преимущества FSD:

  • Масштабируемость: FSD облегчает масштабирование больших приложений, так как новые фичи можно добавлять в виде отдельных слайсов, не затрагивая существующий код.
  • Поддерживаемость: FSD улучшает поддерживаемость кодовой базы благодаря четкой структуре, инкапсуляции и разделению ответственности.
  • Переиспользуемость: Слайсы и слои FSD, особенно entities и shared, спроектированы для переиспользования в разных частях приложения и даже в разных проектах.
  • Разработка в команде: FSD облегчает параллельную разработку в команде, так как разные команды могут работать над разными слайсами независимо.
  • Тестируемость: Изоляция слайсов и слоев упрощает юнит-тестирование и интеграционное тестирование отдельных частей приложения.
  • Гибкость и адаптивность: FSD позволяет гибко адаптировать архитектуру приложения к изменяющимся требованиям бизнеса.

FSD и Next.js App Router:

FSD архитектура хорошо сочетается с Next.js App Router, который также ориентирован на организацию приложения по фичам (страницы и компоненты роутов в директории app). Можно организовать слайсы FSD внутри директории app или на верхнем уровне проекта, в зависимости от размера и сложности приложения. Server Components и Client Components в Next.js также хорошо вписываются в концепцию слоев FSD. Server Components могут использоваться в слоях pages, widgets, features, entities, а Client Components - преимущественно во features и widgets для реализации интерактивности.

Вопрос 22: Применяете ли вы FSD в Next.js проектах? Какие особенности адаптации FSD для Next.js вы используете?

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

Ответ кандидата: Предлагает интересную адаптацию FSD для Next.js. Кандидат предложил разделять Server Components и Client Components на уровне сущностей, создавая папки server и client внутри сущностей, что является необычным, но интересным подходом.

Правильный ответ (адаптация FSD для Next.js):

FSD отлично подходит для организации Next.js проектов, особенно с использованием App Router, так как оба подхода ориентированы на фиче-ориентированную архитектуру. Вот несколько особенностей адаптации FSD для Next.js:

  1. Организация слайсов в директории app (или на верхнем уровне проекта):

    • Слайсы (фичи) могут быть организованы внутри директории app в Next.js App Router, соответствуя структуре маршрутов приложения. Например, слайс user-profile может быть размещен в app/user-profile.
    • Альтернативно, слайсы можно разместить на верхнем уровне проекта (рядом с директорией app) и импортировать компоненты из слайсов в страницы и компоненты роутов в app. Выбор зависит от размера и сложности приложения. Для крупных проектов, верхнеуровневая организация слайсов может быть более предпочтительной для лучшей изоляции и разделения ответственности.
  2. Слой pages в FSD и Next.js app directory:

    • Слой pages в FSD соответствует директории app в Next.js App Router. Компоненты, размещенные непосредственно в директории app (и поддиректориях), играют роль "страниц" или "видов" приложения в FSD.
  3. Server Components и Client Components в слоях FSD:

    • Server Components - предпочтительнее по умолчанию: Старайтесь использовать Server Components как можно больше во всех слоях FSD, особенно в pages, widgets, features, entities. Server Components обеспечивают лучшую производительность, безопасность и меньший размер бандла.
    • Client Components - только для интерактивности: Используйте Client Components только там, где необходима интерактивность на стороне клиента (в слоях features, widgets, реже в pages). Маркируйте Client Components директивой 'use client'.
    • Размещение Server Components и Client Components:
      • Внутри слайсов и слоев FSD, Client Components и Server Components могут находиться рядом друг с другом, в разных файлах. Например, в директории features/user-profile, можно иметь UserProfileCard.server.tsx (Server Component) и UserProfileEditButton.client.tsx (Client Component).
      • Разделение на директории server и client (как предложил кандидат) - возможный вариант, но не является общепринятой практикой FSD. Это может быть полезно для более явного разделения ответственности и предотвращения случайного импорта Client Components в Server Components, но может усложнить структуру директорий. Если используется разделение на директории, то компоненты в server директории будут Server Components по умолчанию, а компоненты в client - Client Components (с директивой 'use client').
  4. Использование Next.js API Routes в слое app/api:

    • API Routes в Next.js (app/api directory) могут рассматриваться как часть слоя app в FSD или как отдельный бэкенд-слой, взаимодействующий с frontend-приложением. В зависимости от сложности бэкенда, API Routes можно организовать также по FSD принципам (разделение на слайсы backend-фич, слои бэкенд-логики).
    • Server Actions в Next.js: Server Actions предоставляют альтернативный способ взаимодействия Client Components с сервером, минуя API Routes в некоторых случаях. Server Actions могут быть определены в Server Components и вызываться из Client Components. Server Actions также можно организовывать по FSD принципам, размещая их в соответствующих слайсах и слоях.
  5. Файловая структура и именование файлов:

    • Следуйте FSD принципам именования файлов и директорий: Используйте осмысленные имена, отражающие назначение файла или директории. Например, UserProfileCard/UserProfileCard.tsx, UserProfileService.ts, UserProfileStyles.module.css.
    • Разделение по файлам по типу сущности: Внутри слоев и слайсов FSD, разделяйте сущности по файлам по их типу (компоненты, сервисы, стили, типы, константы, тесты и т.д.).
  6. Public API и index.ts files:

    • Продолжайте использовать Public API и index.ts файлы для явного определения интерфейсов и инкапсуляции внутренних деталей слайсов и слоев FSD в Next.js проектах.
  7. Абстракция данных и доменная логика в entities:

    • Слой entities особенно важен в Next.js проектах. Разместите в entities доменные модели данных, бизнес-логику (валидацию, трансформацию данных), и типы, которые используются в разных частях приложения. Это поможет отделить frontend-логику от доменной логики и упростить переиспользование сущностей.

Пример FSD структуры в Next.js App Router (верхнеуровневые слайсы):

app/
page.tsx
layout.tsx
user-profile/
components/
UserProfileCard.server.tsx
UserProfileEditButton.client.tsx
services/
userProfileService.ts
entities/
user.ts
index.ts (public API для user-profile slice)
article-comments/
...
api/
... (API Routes, можно организовать по FSD слайсам)
shared/
ui/
button/
input/
...
lib/
utils.ts
hooks.ts
styles/
globals.css
types/
...

Заключение:

FSD предоставляет ценные принципы для организации frontend-проектов, которые хорошо применимы и к Next.js. Адаптация FSD для Next.js включает в себя организацию слайсов и слоев с учетом файловой структуры Next.js App Router, разделение Server Components и Client Components, и использование возможностей Next.js API и Server Actions. Применение FSD в Next.js проектах способствует созданию масштабируемых, поддерживаемых и гибких приложений.

Tailwind CSS

Вопрос 23: Почему вам нравится Tailwind CSS? Расскажите о преимуществах Tailwind CSS.

Таймкод: 00:44:28

Ответ кандидата: Хорошее понимание преимуществ Tailwind CSS. Кандидат выделил преимущества, такие как принудительное разделение стилей и логики, скорость разработки, декларативность и кастомизацию.

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

Tailwind CSS - это утилитарно-ориентированный CSS-фреймворк, который предоставляет огромное количество готовых к использованию CSS-классов утилит. Вместо того чтобы писать собственный CSS с нуля или использовать семантические классы, в Tailwind CSS вы комбинируете классы-утилиты непосредственно в HTML-разметке для стилизации элементов.

Преимущества Tailwind CSS (по сравнению с традиционными CSS-подходами и семантическими CSS-фреймворками):

  1. Быстрая разработка (Rapid Development):

    • Огромное количество готовых утилит: Tailwind CSS предоставляет тысячи готовых CSS-классов утилит для решения практически любых задач стилизации. Нет необходимости писать CSS с нуля для большинства стилей.
    • Стиль непосредственно в HTML: Стилизация происходит непосредственно в HTML-разметке, путем добавления классов-утилит. Не нужно переключаться между HTML и CSS файлами.
    • Фокус на контенте и функциональности: Tailwind CSS ускоряет процесс стилизации, позволяя разработчикам больше сосредоточиться на контенте и функциональности приложения, а не на написании CSS.
    • Быстрый прототипинг и итерации: Tailwind CSS отлично подходит для быстрого прототипирования и итераций, так как стили можно менять и дорабатывать очень быстро, прямо в HTML.
  2. Консистентность дизайна (Design Consistency):

    • Ограниченный набор стилей: Tailwind CSS поощряет использование ограниченного, предопределенного набора стилей и цветовой палитры, что способствует консистентности дизайна в приложении.
    • Дизайн-система "из коробки": Tailwind CSS фактически предоставляет базовую дизайн-систему "из коробки", что упрощает создание UI с единым стилем.
    • Легко поддерживать дизайн-систему: Если в проекте используется дизайн-система, Tailwind CSS упрощает ее реализацию и поддержку, так как утилиты можно легко кастомизировать и расширять в tailwind.config.js.
  3. Производительность (Performance):

    • Меньший CSS-бандл в production: Tailwind CSS использует процесс "tree-shaking" (удаление неиспользуемого кода) в production сборке. В production бандл включаются только те CSS-утилиты, которые реально используются в проекте. Это может значительно уменьшить размер CSS-бандла по сравнению с традиционными CSS-фреймворками или самописным CSS, где в бандл могут попадать неиспользуемые стили.
    • Минимизация CSS-специфичности: Утилитарные классы Tailwind CSS, как правило, имеют низкую специфичность, что уменьшает проблемы, связанные с CSS-специфичностью и конфликтами стилей.
  4. Кастомизация (Customization):

    • Конфигурационный файл tailwind.config.js: Tailwind CSS легко кастомизируется через конфигурационный файл tailwind.config.js. Можно настроить:
      • Цветовую палитру.
      • Типографику.
      • Брейкпоинты (media queries).
      • Шкала отступов и размеров.
      • Трансформации, тени, фильтры и другие CSS-свойства.
      • Добавить собственные утилиты и стили.
    • Расширяемость (Extensibility): Tailwind CSS можно расширять, добавляя пользовательские утилиты, компоненты и плагины.
  5. Чистый HTML и разделение ответственности (Clean HTML and Separation of Concerns):

    • HTML остается чистым и семантичным: Вместо добавления стилей непосредственно в CSS-файлы, стилизация происходит путем добавления классов-утилит в HTML-разметку. HTML код остается чистым и семантичным, и не смешивается с CSS-стилями, как при использовании inline-styles.
    • Разделение ответственности: Tailwind CSS явно разделяет ответственность за структуру контента (HTML) и его отображение (Tailwind CSS утилиты).
  6. Сообщество и экосистема:

    • Большое и активное сообщество: Tailwind CSS имеет большое и активное сообщество разработчиков, предоставляющее поддержку, документацию, ресурсы и плагины.
    • Экосистема инструментов и плагинов: Вокруг Tailwind CSS сложилась богатая экосистема инструментов и плагинов, расширяющих его возможности (например, Tailwind UI - библиотека готовых компонентов, Headless UI - unstyled components, плагины для типографики, форм, анимаций и т.д.).

Недостатки Tailwind CSS (и утилитарно-ориентированных CSS-фреймворков в целом):

  • Много "классов-мешанины" в HTML (Class Clutter): HTML-разметка может стать многословной и "замусоренной" классами-утилитами, особенно для сложных компонентов с большим количеством стилей. Читаемость HTML может снизиться.
  • Кривая обучения: Для эффективного использования Tailwind CSS, нужно изучить большое количество классов-утилит и принципы их комбинации. На начальном этапе может потребоваться время на освоение фреймворка.
  • Ограниченность абстракций (Lack of Abstractions): Tailwind CSS предоставляет утилиты, но не предлагает готовых семантических компонентов или абстракций высокого уровня. Разработчикам приходится создавать собственные компоненты и паттерны стилизации, комбинируя утилиты.
  • Возможность злоупотребления и нарушения консистентности: Несмотря на то, что Tailwind CSS поощряет консистентность, разработчики могут злоупотреблять фреймворком и создавать неконсистентный UI, если не придерживаться дизайн-системы и best practices.

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

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

Когда Tailwind CSS может быть не лучшим выбором:

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

В заключение:

Tailwind CSS - это мощный и гибкий утилитарно-ориентированный CSS-фреймворк, который предлагает множество преимуществ, особенно для скорости разработки, консистентности дизайна и производительности. Однако, как и любой инструмент, Tailwind CSS имеет свои сильные и слабые стороны, и его выбор должен быть обусловлен требованиями конкретного проекта и предпочтениями команды разработчиков.

Оценка Кандидата

В целом, кандидат показал себя как уверенный Middle Frontend-разработчик с хорошим знанием основ HTML, CSS, JavaScript, TypeScript, React и Next.js. Он уверенно отвечал на большинство вопросов, демонстрируя понимание ключевых концепций и принципов frontend-разработки.

Сильные стороны кандидата:

  • Хорошее понимание фундаментальных концепций frontend-разработки: CSS специфичность, доступность, псевдоклассы/псевдоэлементы, асинхронность в JavaScript, утилитарные типы TypeScript, React Server Components, FSD архитектура.
  • Умение решать простые задачи live coding: Кандидат успешно реализовал функцию delay и предложил несколько вариантов решения задачи с асинхронным циклом.
  • Понимание проблем и best practices React: Кандидат верно определил проблемы в коде React компонентов (бесконечный цикл ререндеров, использование индекса в качестве key) и предложил адекватные решения.
  • Интерес к архитектурным вопросам и FSD: Кандидат показал интерес к архитектуре frontend-приложений и знакомство с FSD, что является важным для Middle разработчика.
  • Осознание своих пробелов и готовность учиться: Кандидат честно признался в пробелах в знаниях (например, в вопросе про forEach и асинхронность) и выразил готовность учиться и развиваться.

Зоны роста кандидата:

  • Более глубокое понимание асинхронности в JavaScript: Некоторая неуверенность в вопросах про forEach и асинхронность. Рекомендуется углубить знания в области Promise, async/await, Event Loop, асинхронных итераторов и генераторов.
  • Более уверенное владение React Suspense и use Hook: Недостаточно уверенный ответ на вопрос про React Suspense и use Hook. Рекомендуется изучить новые возможности React 18+ в области асинхронности и Server Components.
  • Declaration Merging в TypeScript: Не знаком с концепцией Declaration Merging. Рекомендуется изучить эту возможность TypeScript, так как она полезна для расширения типов и организации кода.

Общая оценка:

Уровень кандидата - уверенный Middle Frontend-разработчик, с потенциалом роста до Senior. Кандидат готов к работе над сложными frontend-проектами и обладает необходимыми знаниями и навыками для успешной работы в команде. Небольшие пробелы в знаниях являются нормальными для Middle уровня и могут быть быстро устранены в процессе работы и обучения.

Рекомендации для кандидата:

  • Углубить знания в области асинхронности JavaScript: Повторить Promise, async/await, Event Loop, асинхронные итераторы и генераторы.
  • Изучить React 18+ и Server Components: Разобраться с новыми возможностями React 18+, особенно React Server Components, Suspense, use Hook и их применением для работы с асинхронными данными.
  • Ознакомиться с Declaration Merging в TypeScript: Изучить концепцию Declaration Merging и ее применение для расширения типов и организации кода.
  • Продолжать практиковаться в live coding и решении алгоритмических задач: Регулярно практиковаться в решении задач на JavaScript и TypeScript, чтобы улучшить навыки live coding и алгоритмическое мышление.
  • Изучить best practices FSD и архитектуры frontend-приложений: Продолжать углублять знания в области FSD и других современных архитектур frontend-приложений, чтобы стать более опытным и квалифицированным разработчиком.