Собеседование Middle Frontend-разработчика + Live Coding | JS, Typescript, React, FSD, Next.js
Сегодня мы разберем интересное собеседование 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-селектора и применяет стили с наивысшей специфичностью.
Уровни специфичности (в порядке возрастания):
- Селекторы элементов (теги), например,
p,div,span. - Селекторы классов, например,
.container,.button. - Селекторы атрибутов, например,
[type="text"]. - Псевдоклассы, например,
:hover,:focus. - Селекторы ID, например,
#header,#navigation. - Инлайновые стили, заданные непосредственно в HTML-атрибуте
style. !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
Объяснение:
console.log('Дан');(первый): Первыйconsole.log('Дан');выполняется синхронно и выводится в консоль сразу.values.forEach(...): Запускается циклforEach. Важно понимать, чтоforEachне является асинхронным и не ждет завершения асинхронных операций внутри своего колбэка.- Асинхронные операции в
forEach: Для каждого элемента массиваvalues,forEachвызывает асинхронную функциюasync (val) => { ... }. Эти асинхронные функции начинают выполняться параллельно, ноforEachне ждет их завершения. console.log('Дан');(второй): ПосколькуforEachне ждет асинхронных операций, циклforEachбыстро завершается, и второйconsole.log('Дан');выполняется сразу после первого, до того, как промисы отdelayзарезолвятся.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.
console.log('Дан');(первый): Выполняется синхронно.for (const val of values): Начинается последовательная итерация массиваvalues.const result = await delay(1000, val);: Для каждой итерации цикла,await delay(1000, val);ожидает разрешения промисаdelayперед переходом к следующей итерации. Таким образом, код приостанавливает выполнение на 1 секунду.console.log(result);: После задержки и разрешения промиса, значениеresult(то естьval) выводится в консоль.- Цикл
for...ofповторяется для каждого элемента массиваvaluesпоследовательно, с задержкой в 1 секунду между каждой итерацией. 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 для создания цепочки промисов.
console.log('Дан');(первый): Выполняется синхронно.values.reduce(...): Используетсяreduceдля последовательного выполнения асинхронных операций.async (promiseChain, val) => { ... }: Колбэк-функцияreduceявляется асинхронной.promiseChainаккумулирует промис из предыдущей итерации (илиPromise.resolve()для первой итерации).await promiseChain;: Перед началом текущей асинхронной операции,await promiseChain;ожидает завершения промиса из предыдущей итерации, обеспечивая последовательное выполнение.const result = await delay(1000, val);: Выполняется асинхронная операцияdelayс задержкой в 1 секунду.console.log(result);: После задержки и разрешения промиса, значениеresultвыводится в консоль.Promise.resolve()(initialValue):Promise.resolve()используется как начальное значение дляreduce, чтобы запустить цепочку промисов.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.
Объяснение:
const set = new Set([...]): Создается новый объектSet.Set- это коллекция, которая хранит только уникальные значения.[{ value: 1 }, { value: 2 }, { value: 1 }]: ВSetпередается массив, содержащий три объекта. Важно понимать, что в JavaScript объекты сравниваются по ссылке, а не по значению.- Уникальность объектов в
Set: Даже несмотря на то, что первый и третий объекты имеют одинаковое свойствоvalue: 1, это разные объекты в памяти с разными ссылками. Поэтому с точки зренияSet, они считаются разными. Array.from(set):Array.from(set)преобразуетSetобратно в массив. В массиве будут только уникальные значения изSet. В данном случае, это будут два объекта:{ value: 1 }(первый экземпляр) и{ value: 2 }..length: Свойство.lengthмассива возвращает количество элементов в массиве. В массиве, полученном изSet, будет 2 элемента.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.
Объяснение:
async function a() { return b(); }: Функцияaобъявлена как асинхронная (async). Она вызывает функциюbи возвращает результат ее вызова. Посколькуbтакже асинхронная функция,aвозвращает промис, который зарезолвится в значение, возвращенное функциейb.async function b() { return 1; }: Функцияbобъявлена как асинхронная и возвращает примитивное значение1. Когда асинхронная функция возвращает примитивное значение, JavaScript автоматически оборачивает его в зарезолвленный промис. То есть,return 1;эквивалентноreturn Promise.resolve(1);.async function main() { ... }: Функцияmainтакже асинхронная.const result = await a();: Внутриmain,await a();вызывает функциюaи ждет разрешения промиса, который возвращаетa. Как мы выяснили,aвозвращает промис, который резолвится в значение, возвращенноеb, аbвозвращает промис, который резолвится в1.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>
);
}
Объяснение:
fetchData(): Асинхронная функция, которая отправляет запрос к API и возвращает промис, резолвящийся с полученными данными.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), и рендеринг продолжится, отображая данные.
MyPage: ОборачиваетDataComponentвSuspenseдля обработки состояния загрузки данных.
Ключевые моменты:
useHook - только в Server Components:useHook может быть вызван только внутри 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).
Ключевые различия в таблице:
| Feature | Server 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).
Объяснение бесконечного цикла:
- Инициализация: Компонент
CounterComponentрендерится впервые.countинициализируется значением0. - Первый клик на "Increment":
- Функция
incrementвызывается,setCount(count + 1)обновляет состояниеcountдо1. - React запускает процесс ререндеринга компонента.
- Функция
- Ререндеринг и
useEffect:- Компонент рендерится с новым значением
count = 1. useEffectвызывается, так какcount(зависимостьuseEffect) изменился.- Условие
if (count === 5)не выполняется, так какcountравен1. console.log('Count updated:', count);выводит "Count updated: 1" в консоль.
- Компонент рендерится с новым значением
- Второй, третий, четвертый клики: Аналогично, при каждом клике
countувеличивается, компонент ререндерится,useEffectвызывается, условиеif (count === 5)не выполняется,console.logсрабатывает. - Пятый клик на "Increment":
- Функция
incrementвызывается,setCount(count + 1)обновляет состояниеcountдо5. - React запускает ререндеринг.
- Функция
- Ререндеринг и
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для создания refisResettingRef, который будет отслеживать, находится ли компонент в процессе сброса счетчика.useEffect(первый):if (count === 5 && !isResettingRef.current): Условие изменено. Теперь сброс счетчика происходит только еслиcount === 5иisResettingRef.currentisfalse. Это предотвращает повторный сброс счетчика при каждом ререндере после установки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 может быть приемлемо только в очень ограниченных случаях, когда гарантированно, что:
- Список является статичным и никогда не меняется после первоначального рендеринга.
- Порядок элементов в списке никогда не будет изменяться (добавление, удаление, сортировка, фильтрация не происходят).
В большинстве реальных приложений, эти условия не выполняются, и использование индекса в качестве 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>
);
}
Объяснение исправления:
initialItemsprop: Prop компонента переименован вinitialItemsдля ясности и чтобы подчеркнуть, что это начальный список элементов.useState(initialItems):initialItemsиспользуется для инициализации состоянияlistItems.newItem.id = Math.random().toString(): При добавлении нового элемента, генерируется уникальныйidс помощьюMath.random().toString().toString()преобразует число в строку, так какkeyprop ожидает строковое значение. В реальных приложениях, лучше использовать более надежные способы генерации уникальных ID, например, UUID библиотеки.key={item.id}: Теперь в<li>элементах используетсяitem.idв качествеkeyprop. Это обеспечивает React стабильным и уникальным идентификатором для каждого элемента списка.- Функциональное обновление состояния
setListItems: ВaddItemиспользуется функциональное обновление состоянияsetListItems(prevListItems => [...prevListItems, newItem]);. Это лучшая практика для обновления состояния, основанного на предыдущем состоянии, особенно при работе с асинхронными обновлениями состояния или при использовании strict mode React.
Дополнительные улучшения (не связанные с key, но рекомендуется):
- Использовать более надежный способ генерации ID:
Math.random()хоть и генерирует случайные числа, но не гарантирует абсолютную уникальность, особенно при большом количестве элементов или в сложных сценариях. Для production-приложений, рекомендуется использовать UUID (Universally Unique Identifier) библиотеки, такие какuuidилиnanoid, для генерации гарантированно уникальных ID. - Проверка наличия
keyprop линтером: Настроить линтер (например, eslint-plugin-react-hooks) для предупреждения или ошибки, еслиkeyprop отсутствует при рендеринге списков. Это поможет предотвратить случайное пропущениеkeyprop и связанные с этим проблемы.
Вопрос 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 при гидратации и ошибкам. - Проблемы с
keyprop и обновлением списка: Несоответствие ID между сервером и клиентом нарушит механизмkeyprop в React, так как React не сможет правильно сопоставить элементы списка, отрендеренные на сервере, с элементами на клиенте. Это может привести к тем же проблемам, что и использование индекса в качествеkey(неправильное обновление компонентов, потеря состояния, проблемы с фокусом и анимациями).
Решение - UUID (Universally Unique Identifier):
UUID - это стандарт для генерации глобально уникальных 128-битных идентификаторов. UUID генерируются с использованием алгоритмов, которые гарантируют практически нулевую вероятность коллизии (совпадения) ID.
Преимущества использования UUID для генерации ID в Server Components:
- Генерация на сервере: UUID можно генерировать на сервере в Server Components перед рендерингом, и эти UUID будут включены в HTML, отправленный браузеру.
- Консистентность на сервере и клиенте: Поскольку UUID генерируются детерминированно (или псевдо-детерминированно, в зависимости от библиотеки), UUID, сгенерированные на сервере, будут консистентны и распознаны React на клиенте во время гидратации. Это избегает проблем mismatch гидратации и обеспечивает правильную работу
keyprop. - Глобальная уникальность: UUID гарантируют глобальную уникальность, что важно для больших приложений и распределенных систем, где нужно обеспечить уникальность идентификаторов в разных частях приложения и между разными экземплярами приложения.
Как использовать UUID в React Server Components (пример с библиотекой uuid):
-
Установите UUID библиотеку:
npm install uuid -
Импортируйте 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:
-
Разделение на слайсы (Slices / Features):
- Приложение разделяется на независимые "слайсы" или "фичи", каждый из которых представляет собой отдельную бизнес-функциональность или доменную область приложения.
- Примеры слайсов:
user-profile,article-comments,product-catalog,authentication,settings. - Независимость и изоляция: Слайсы должны быть максимально независимы друг от друга. Изменения в одном слайсе должны минимально влиять на другие слайсы.
- Слабая связанность (Loose Coupling): Связи между слайсами должны быть минимальными и явно определенными (например, через public API).
- Переиспользуемость: Слайсы могут быть переиспользованы в разных частях приложения или даже в разных проектах.
-
Слоистая архитектура (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слой предназначен для разделения общих ресурсов и минимизации дублирования кода. Не должен зависеть ни от каких других слоев приложения, включая слайсы (максимальная переиспользуемость).
-
-
Public API (Публичный API):
- Каждый слайс и каждый слой должен иметь явно определенный Public API, который определяет, что именно можно использовать из этого слайса или слоя снаружи.
- Инкапсуляция: Все внутренние детали реализации слайса или слоя должны быть скрыты за Public API (инкапсулированы). Это позволяет изменять внутреннюю реализацию без нарушения работы других частей приложения, которые используют Public API.
- Управляемые зависимости: Использование Public API позволяет контролировать и ограничивать зависимости между слайсами и слоями. Зависимости должны быть направлены только через Public API.
index.ts(илиindex.js) file: Public API обычно реализуется через файлindex.ts(илиindex.js) в корне каждого слоя или слайса, который экспортирует только те сущности, которые предназначены для публичного использования.
-
Принцип "Импорт только снизу вверх" (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:
-
Организация слайсов в директории
app(или на верхнем уровне проекта):- Слайсы (фичи) могут быть организованы внутри директории
appв Next.js App Router, соответствуя структуре маршрутов приложения. Например, слайсuser-profileможет быть размещен вapp/user-profile. - Альтернативно, слайсы можно разместить на верхнем уровне проекта (рядом с директорией
app) и импортировать компоненты из слайсов в страницы и компоненты роутов вapp. Выбор зависит от размера и сложности приложения. Для крупных проектов, верхнеуровневая организация слайсов может быть более предпочтительной для лучшей изоляции и разделения ответственности.
- Слайсы (фичи) могут быть организованы внутри директории
-
Слой
pagesв FSD и Next.jsappdirectory:- Слой
pagesв FSD соответствует директорииappв Next.js App Router. Компоненты, размещенные непосредственно в директорииapp(и поддиректориях), играют роль "страниц" или "видов" приложения в FSD.
- Слой
-
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').
- Внутри слайсов и слоев FSD, Client Components и Server Components могут находиться рядом друг с другом, в разных файлах. Например, в директории
- Server Components - предпочтительнее по умолчанию: Старайтесь использовать Server Components как можно больше во всех слоях FSD, особенно в
-
Использование Next.js API Routes в слое
app/api:- API Routes в Next.js (
app/apidirectory) могут рассматриваться как часть слоя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 принципам, размещая их в соответствующих слайсах и слоях.
- API Routes в Next.js (
-
Файловая структура и именование файлов:
- Следуйте FSD принципам именования файлов и директорий: Используйте осмысленные имена, отражающие назначение файла или директории. Например,
UserProfileCard/UserProfileCard.tsx,UserProfileService.ts,UserProfileStyles.module.css. - Разделение по файлам по типу сущности: Внутри слоев и слайсов FSD, разделяйте сущности по файлам по их типу (компоненты, сервисы, стили, типы, константы, тесты и т.д.).
- Следуйте FSD принципам именования файлов и директорий: Используйте осмысленные имена, отражающие назначение файла или директории. Например,
-
Public API и
index.tsfiles:- Продолжайте использовать Public API и
index.tsфайлы для явного определения интерфейсов и инкапсуляции внутренних деталей слайсов и слоев FSD в Next.js проектах.
- Продолжайте использовать Public API и
-
Абстракция данных и доменная логика в
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-фреймворками):
-
Быстрая разработка (Rapid Development):
- Огромное количество готовых утилит: Tailwind CSS предоставляет тысячи готовых CSS-классов утилит для решения практически любых задач стилизации. Нет необходимости писать CSS с нуля для большинства стилей.
- Стиль непосредственно в HTML: Стилизация происходит непосредственно в HTML-разметке, путем добавления классов-утилит. Не нужно переключаться между HTML и CSS файлами.
- Фокус на контенте и функциональности: Tailwind CSS ускоряет процесс стилизации, позволяя разработчикам больше сосредоточиться на контенте и функциональности приложения, а не на написании CSS.
- Быстрый прототипинг и итерации: Tailwind CSS отлично подходит для быстрого прототипирования и итераций, так как стили можно менять и дорабатывать очень быстро, прямо в HTML.
-
Консистентность дизайна (Design Consistency):
- Ограниченный набор стилей: Tailwind CSS поощряет использование ограниченного, предопределенного набора стилей и цветовой палитры, что способствует консистентности дизайна в приложении.
- Дизайн-система "из коробки": Tailwind CSS фактически предоставляет базовую дизайн-систему "из коробки", что упрощает создание UI с единым стилем.
- Легко поддерживать дизайн-систему: Если в проекте используется дизайн-система, Tailwind CSS упрощает ее реализацию и поддержку, так как утилиты можно легко кастомизировать и расширять в
tailwind.config.js.
-
Производительность (Performance):
- Меньший CSS-бандл в production: Tailwind CSS использует процесс "tree-shaking" (удаление неиспользуемого кода) в production сборке. В production бандл включаются только те CSS-утилиты, которые реально используются в проекте. Это может значительно уменьшить размер CSS-бандла по сравнению с традиционными CSS-фреймворками или самописным CSS, где в бандл могут попадать неиспользуемые стили.
- Минимизация CSS-специфичности: Утилитарные классы Tailwind CSS, как правило, имеют низкую специфичность, что уменьшает проблемы, связанные с CSS-специфичностью и конфликтами стилей.
-
Кастомизация (Customization):
- Конфигурационный файл
tailwind.config.js: Tailwind CSS легко кастомизируется через конфигурационный файлtailwind.config.js. Можно настроить:- Цветовую палитру.
- Типографику.
- Брейкпоинты (media queries).
- Шкала отступов и размеров.
- Трансформации, тени, фильтры и другие CSS-свойства.
- Добавить собственные утилиты и стили.
- Расширяемость (Extensibility): Tailwind CSS можно расширять, добавляя пользовательские утилиты, компоненты и плагины.
- Конфигурационный файл
-
Чистый HTML и разделение ответственности (Clean HTML and Separation of Concerns):
- HTML остается чистым и семантичным: Вместо добавления стилей непосредственно в CSS-файлы, стилизация происходит путем добавления классов-утилит в HTML-разметку. HTML код остается чистым и семантичным, и не смешивается с CSS-стилями, как при использовании inline-styles.
- Разделение ответственности: Tailwind CSS явно разделяет ответственность за структуру контента (HTML) и его отображение (Tailwind CSS утилиты).
-
Сообщество и экосистема:
- Большое и активное сообщество: 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 и
useHook: Недостаточно уверенный ответ на вопрос проReact SuspenseиuseHook. Рекомендуется изучить новые возможности 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,useHook и их применением для работы с асинхронными данными. - Ознакомиться с Declaration Merging в TypeScript: Изучить концепцию Declaration Merging и ее применение для расширения типов и организации кода.
- Продолжать практиковаться в live coding и решении алгоритмических задач: Регулярно практиковаться в решении задач на JavaScript и TypeScript, чтобы улучшить навыки live coding и алгоритмическое мышление.
- Изучить best practices FSD и архитектуры frontend-приложений: Продолжать углублять знания в области FSD и других современных архитектур frontend-приложений, чтобы стать более опытным и квалифицированным разработчиком.