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

Фронтенд собеседование 2025 | Junior Frontend | Реальные вопросы и задачи

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

Сегодня мы разберём собеседование на позицию Junior Frontend-разработчика, в ходе которого кандидат Владислав демонстрирует базовое понимание HTML, CSS, JavaScript и React, но испытывает заметные трудности с практикой — выравниванием элементов, написанием собственного метода map, реализацией задачи на палиндром и созданием интерактивного компонента списка контактов. Несмотря на теоретическую подкованность и способность объяснить концепции вроде замыканий и хука useEffect, кандидат часто теряется при написании кода, нуждается в подсказках и допускает ошибки, связанные с волнением и недостатком практического опыта.

Вопрос 1. Что такое HTML?

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

Ответ собеседника: Правильный. HTML — это язык гипертекстовой разметки.

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

HTML (HyperText Markup Language) — это стандартный язык гипертекстовой разметки, используемый для создания и структурирования веб-страниц. Он определяет структуру содержимого страницы с помощью тегов и атрибутов, позволяя браузерам отображать текст, изображения, ссылки и другие элементы. HTML не является языком программирования, а является языком разметки, который описывает структуру документа. Современная версия — HTML5 — поддерживает мультимедийные элементы, семантические теги и API для создания интерактивных веб-приложений.

Вопрос 2. Какие существуют варианты подключения CSS к HTML-странице?

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

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

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

Существует три основных способа подключения CSS к HTML-странице:

1. Внешняя таблица стилей (External CSS)

Стили хранятся в отдельном файле с расширением .css и подключаются через тег <link> внутри секции <head>.

<head>
<link rel="stylesheet" href="styles.css">
</head>

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

2. Внутренняя таблица стилей (Internal CSS)

Стили размещаются непосредственно в HTML-документе внутри тега <style>, который помещается в секцию <head>.

<head>
<style>
body {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
h1 {
color: navy;
}
</style>
</head>

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

3. Инлайн-стили (Inline CSS)

Стили задаются непосредственно на HTML-элементе через атрибут style.

<p style="color: red; font-size: 16px;">Этот текст будет красным.</p>

Инлайн-стили имеют наивысший приоритет (за исключением !important), но их использование считается плохой практикой, так как смешивает структуру и оформление, затрудняет поддержку и не позволяет переиспользовать стили.

Дополнительный способ: @import

CSS также можно импортировать внутри другого CSS-файла или тега <style> с помощью директивы @import.

@import url('reset.css');
@import url('fonts.css');

Этот способ используется реже, так как может негативно влиять на производительность загрузки страницы — каждый @import создаёт дополнительный HTTP-запрос и блокирует параллельную загрузку стилей.

Вопрос 3. Как разместить квадрат по центру экрана с помощью CSS Flexbox?

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

Ответ собеседника: Неполный. Кандидат использовал Flexbox с justify-content и align-items по центру, но задал ширину 100% контейнеру, что было излишним.

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

Для центрирования элемента по центру экрана с помощью Flexbox необходимо сделать контейнер (обычно body) flex-контейнером и использовать свойства выравнивания.

Основное решение:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

.square {
width: 100px;
height: 100px;
background-color: navy;
}
</style>
</head>
<body>
<div class="square"></div>
</body>
</html>

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

  • margin: 0 — сброс стандартных отступов body, которые браузер добавляет по умолчанию.
  • min-height: 100vh — задаёт высоту контейнера равной высоте вьюпорта. Именно min-height, а не height, чтобы при большом количестве содержимого контейнер мог растягиваться.
  • display: flex — превращает контейнер в flex-контейнер.
  • justify-content: center — центрирует элемент по главной оси (горизонтали).
  • align-items: center — центрирует элемент по поперечной оси (вертикали).

Почему width: 100% было излишним:

Свойство width: 100% для body не нужно, поскольку body является блочным элементом и по умолчанию занимает всю доступную ширину родительского элемента (html). Это свойство ничего не добавляет к решению, но загромождает код.

Альтернативный подход с использованием margin: auto:

Если во flex-контейнере только один дочерний элемент, его можно центрировать и без justify-content / align-items:

body {
margin: 0;
min-height: 100vh;
display: flex;
}

.square {
margin: auto;
width: 100px;
height: 100px;
background-color: navy;
}

Свойство margin: auto во flex-контейнере автоматически распределяет свободное пространство равномерно со всех сторон элемента, тем самым центрируя его по обеим осям.

Вопрос 4. Какие ещё способы центрирования элемента по центру экрана в CSS знаете?

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

Ответ собеседного: Неполный. Кандидат предложил margin и Grid, но не смог реализовать центрирование через margin: auto. В итоге сработал способ через абсолютное позиционирование с transform: translate(-50%, -50%).

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

Существует несколько способов центрирования элемента по центру экрана. Рассмотрим каждый из них подробно.

1. CSS Grid

Grid — один из самых лаконичных и современных способов центрирования.

body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
}

Свойство place-items — это сокращение для align-items и justify-items, одновременно центрирующее элемент по обеим осям. Также можно использовать place-content: center, если нужно центрировать всю сетку целиком.

2. Абсолютное позиционирование с transform

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

body {
margin: 0;
min-height: 100vh;
position: relative;
}

.square {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
background-color: navy;
}

Принцип работы: top: 50% и left: 50% размещают левый верхний угол элемента в центре родителя. Затем transform: translate(-50%, -50%) сдвигает элемент назад на половину его собственной ширины и высоты, тем самым центрируя его.

3. Абсолютное позиционирование с margin: auto

Этот способ работает, когда у элемента заданы конкретные размеры.

body {
margin: 0;
min-height: 100vh;
position: relative;
}

.square {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 100px;
height: 100px;
background-color: navy;
}

Ключевой момент: необходимо задать все четыре свойства (top, right, bottom, left) равными нулю, а затем margin: auto распределит оставшееся пространство равномерно со всех сторон. Без заданных width и height элемент растянется на весь контейнер.

4. Flexbox с margin: auto на дочернем элементе

Альтернативный вариант Flexbox, который кандидат упомянул, но не смог реализовать.

body {
margin: 0;
min-height: 100vh;
display: flex;
}

.square {
margin: auto;
width: 100px;
height: 100px;
background-color: navy;
}

Внутри flex-контейнера margin: auto на дочернем элементе поглощает всё свободное пространство равномерно со всех сторон, центрируя элемент по обеим осям. Этот способ особенно полезен, когда нужно центрировать один элемент среди нескольких.

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

СпособНезнание размеровПоддержка браузераЛаконичность
FlexboxДаОтличнаяВысокая
GridДаОтличнаяСамая высокая
Absolute + transformДаОтличнаяСредняя
Absolute + margin: autoНетОтличнаяСредняя

Для современных проектов рекомендуется использовать Flexbox или Grid — они наиболее гибкие и простые в применении.

Вопрос 5. Как работает техника центрирования через position: absolute с установкой top, left, right, bottom в 0?

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

Ответ собеседника: Неполный. Кандидат предложил растянуть блок, задав top: 0, left: 0, right: 0, bottom: 0, но для центрирования содержимого требовалось дополнительно применить Flexbox или другой метод выравнивания.

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

Данная техника основана на особенностях работы абсолютного позиционирования и свойства margin: auto.

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

Когда элементу задаётся position: absolute и все четыре свойства отступа (top, right, bottom, left) устанавливаются в 0, браузер растягивает элемент на весь доступный размер ближайшего позиционированного предка (или viewport, если такого предка нет).

Если при этом у элемента заданы фиксированные width и height, возникает «конфликт» ограничений: браузер не может одновременно растянуть элемент на весь контейнер и соблюсти его фиксированные размеры. В этом случае margin: auto вступает в действие и равномерно распределяет оставшееся свободное пространство со всех сторон, тем самым центрируя элемент.

Полный пример:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
min-height: 100vh;
position: relative;
}

.centered {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 200px;
height: 150px;
background-color: navy;
color: white;
text-align: center;
}
</style>
</head>
<body>
<div class="centered">Центрированный блок</div>
</body>
</html>

Почему это работает:

  • position: absolute извлекает элемент из нормального потока.
  • top: 0; right: 0; bottom: 0; left: 0 — браузер пытается растянуть элемент от каждой границы контейнера до противоположной.
  • Фиксированные width и height ограничивают размер элемента.
  • margin: auto автоматически вычисляет равные отступы со всех сторон, компенсируя разницу между размером контейнера и размером элемента.

Важное условие:

Этот способ работает только при наличии явно заданных размеров (width и height). Если размеры не заданы, элемент растянется на весь контейнер, и центрирование не произойдёт.

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

Данный подход полезен, когда нужно центрировать модальное окно, оверлей или другой элемент поверх содержимого, и при этом не хочется добавлять вложенные контейнеры для Flexbox или Grid. Однако в большинстве современных проектов предпочтительнее использовать Flexbox или Grid, так как они не требуют фиксированных размеров и не извлекают элемент из потока.

Вопрос 6. Что такое JavaScript и как вы его понимаете?

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

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

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

JavaScript — это высокоуровневый, динамический, интерпретируемый (точнее, JIT-компилируемый) язык программирования, изначально созданный для добавления интерактивности веб-страницам в браузере. Сегодня он является одним из самых востребованных языков в мире и используется практически на всех платформах.

Ключевые характеристики JavaScript:

  • Динамическая типизация — тип переменной определяется во время выполнения, а не на этапе компиляции.
  • Прототипное наследование — в отличие от классического ООП, JavaScript использует прототипную модель (хотя синтаксис class добавлен в ES6).
  • Функции как объекты первого класса — функции можно передавать как аргументы, возвращать из других функций и присваивать переменным.
  • Событийно-ориентированная модель — JavaScript работает на основе событий и асинхронного выполнения.
  • JIT-компиляция — современные движки (V8, SpiderMonkey) не просто интерпретируют код, а компилируют его в машинный код во время выполнения для повышения производительности.

Где используется JavaScript:

  • Frontend — интерактивность веб-страниц (React, Vue, Angular).
  • Backend — серверные приложения на Node.js.
  • Мобильная разработка — React Native, Ionic.
  • Десктоп-приложения — Electron.
  • IoT и встраиваемые системы — Johnny-Five, Espruino.

Пример кода:

// Асинхронная функция с использованием async/await
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Ошибка при получении данных:', error);
throw error;
}
}

// Замыкание
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2

JavaScript стандартизирован как ECMAScript, и его спецификация регулярно обновляется (ES2015, ES2016, ..., ES2024), добавляя новые возможности и улучшения.

Вопрос 7. Какая типизация у JavaScript — слабая или сильная, статическая или динамическая?

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

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

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

JavaScript имеет слабую (нестрогую) динамическую типизацию. Эти два понятия описывают разные аспекты системы типов и не являются взаимоисключающими.

Динамическая типизация означает, что проверка типов происходит во время выполнения программы, а не на этапе компиляции. Одна и та же переменная может хранить значения разных типов в разные моменты времени.

let value = 42; // number
value = "hello"; // string
value = true; // boolean
value = { a: 1 }; // object

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

console.log("5" + 3); // "53" — число приведено к строке
console.log("5" - 3); // 2 — строка приведена к числу
console.log(true + 1); // 2 — boolean приведён к числу
console.log([] + []); // "" — массивы приведены к строкам
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0 (или "[object Object]" в зависимости от контекста)

Почему это важно:

Слабая типизация — одна из главных причин неожиданного поведения JavaScript. Например, оператор == выполняет неявное приведение типов, тогда как === сравнивает и значение, и тип.

console.log(0 == ""); // true — неявное приведение
console.log(0 === ""); // false — строгое сравнение
console.log(null == undefined); // true
console.log(null === undefined); // false

Терминологическая путаницу:

В сообществе нет единого мнения относительно терминов «слабая» и «сильная» типизация. Часто их используют как синонимы к «нестрогая» и «строгая» типизация. Главное, что нужно понимать: JavaScript автоматически преобразует типы в выражениях, что может приводить к неочевидным результатам.

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

Для минимизации ошибок, связанных со слабой типизацией, рекомендуется использовать строгое сравнение (=== и !==), а также применять TypeScript — надмножество JavaScript со статической типизацией, которое ловит ошибки типов на этапе компиляции.

Вопрос 8. Что такое замыкание в JavaScript?

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

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

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

Замыкание (closure) — это механизм, при котором функция сохраняет доступ к своему лексическому окружению (scope), даже когда она выполняется за пределами этого окружения. Другими словами, функция «замыкает» переменные из того контекста, в котором была создана.

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

В JavaScript каждый вызов функции создаёт новый контекст выполнения со своим набором переменных. Когда внутренняя функция ссылается на переменные внешней функции, движок JavaScript сохраняет ссылку на эти переменные даже после того, как внешняя функция завершила выполнение. Это и есть замыкание.

Базовый пример:

function createGreeting(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}

const sayHello = createGreeting("Hello");
const sayHi = createGreeting("Hi");

console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob")); // "Hi, Bob!"

Функция, возвращённая из createGreeting, замыкает переменную greeting и сохраняет доступ к ней при каждом последующем вызове.

Практическое применение — создание приватных переменных:

function createCounter() {
let count = 0; // приватная переменная

return {
increment: function() {
return ++count;
},
decrement: function() {
return --count;
},
getCount: function() {
return count;
},
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2
console.log(counter.count); // undefined — переменная недострутна извне

Переменная count защищена от прямого доступа извне — это реализация инкапсуляции через замыкания.

Классическая ловушка с циклом и var:

// Неправильно — все функции выведут 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Вывод: 5, 5, 5, 5, 5

// Правильно — через IIFE, создающее замыкание
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
// Вывод: 0, 1, 2, 3, 4

// Или с использованием let (блочная область видимости)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// Вывод: 0, 1, 2, 3, 4

Важные аспекты:

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

Замыкания — один из фундаментальных механизмов JavaScript, лежащий в основе модульного паттерна, каррирования, мемоизации и многих других техник.

Вопрос 9. Реализуйте собственный метод myMap, который работает аналогично нативному методу Array.map, используя прототипы.

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

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

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

Реализация собственного метода myMap, аналогичного нативному Array.prototype.map, включает несколько важных аспектов.

Базовая реализация:

Array.prototype.myMap = function(callback) {
const result = [];

for (let i = 0; i < this.length; i++) {
result.push(callback(this[i], i, this));
}

return result;
};

Здесь this — это массив, на котором вызывается метод. Колбэк принимает три аргумента: текущий элемент, его индекс и ссылку на исходный массив — точно так же, как нативный map.

Полная реализация с обработкой краевых случаев:

Array.prototype.myMap = function(callback, thisArg) {
// Проверка на вызов не от массива
if (this == null) {
throw new TypeError('Array.prototype.myMap called on null or undefined');
}

// Проверка, что callback — функция
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}

const result = [];
const length = this.length;

for (let i = 0; i < length; i++) {
// Пропуск «дыр» в разреженных массивах
if (i in this) {
result.push(callback.call(thisArg, this[i], i, this));
}
}

return result;
};

Что здесь учтено:

  • thisArg — необязательный параметр, задающий контекст выполнения колбэка (аналог второго аргумента нативного map).
  • callback.call(thisArg, ...) — вызов колбэка с привязкой контекста через call.
  • i in this — проверка существования индекса, чтобы корректно обрабатывать разреженные массивы (sparse arrays), где некоторые индексы отсутствуют.
  • Проверки типов, аналогичные нативному методу.

Примеры использования:

// Простое преобразование
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.myMap(function(item) {
return item * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]

// Использование индекса и исходного массива
const withIndex = numbers.myMap(function(item, index, array) {
return item + index;
});
console.log(withIndex); // [1, 3, 5, 7, 9]

// Использование thisArg
const multiplier = {
factor: 3,
multiply: function(arr) {
return arr.myMap(function(item) {
return item * this.factor;
}, this);
}
};
console.log(multiplier.multiply([1, 2, 3])); // [3, 6, 9]

// Цепочка вызовов
const result = [1, 2, 3]
.myMap(x => x * 2)
.myMap(x => x + 1);
console.log(result); // [3, 5, 7]

Важное замечание:

Расширение прототипов встроенных объектов (monkey patching) в реальных проектах считается плохой практикой, так как может конфликтовать с другими библиотеками или будущими обновлениями стандарта. Эта задача решается исключительно в учебных и собеседовательных целях для демонстрации понимания прототипной модели JavaScript.

Вопрос 10. Напишите функцию для проверки, является ли строка палиндромом.

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

Ответ собеседника: Неполный. Кандидат предложил два алгоритма: сравнение символов с начала и конца, а также переворот строки и сравнение. При реализации возникли трудности с методами split, reverse, join. Задача была частично решена с помощью подсказок.

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

Палиндром — это строка, которая читается одинаково слева направо и справа налево. Рассмотрим несколько подходов к решению.

Способ 1: Переворот строки (самый простой)

function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-zа-яё0-9]/g, '');
const reversed = cleaned.split('').reverse().join('');
return cleaned === reversed;
}

console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
console.log(isPalindrome("race a car")); // false
console.log(isPalindrome("шалаш")); // true

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

  • toLowerCase() — приведение к нижнему регистру.
  • replace(/[^a-zа-яё0-9]/g, '') — удаление всех символов, кроме букв и цифр.
  • split('') — разбиение строки на массив символов.
  • reverse() — переворот массива.
  • join('') — сборка массива обратно в строку.

Способ 2: Два указателя (оптимальный по памяти)

function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-zа-яё0-9]/g, '');

let left = 0;
let right = cleaned.length - 1;

while (left < right) {
if (cleaned[left] !== cleaned[right]) {
return false;
}
left++;
right--;
}

return true;
}

Этот подход не создаёт новую строку, а сравнивает символы попарно с обоих концов, двигаясь к центру. Сложность по памяти — O(1) (без учёта строки после очистки).

Способ 3: Рекурсивный

function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-zа-яё0-9]/g, '');

function check(left, right) {
if (left >= right) return true;
if (cleaned[left] !== cleaned[right]) return false;
return check(left + 1, right - 1);
}

return check(0, cleaned.length - 1);
}

Способ 4: Через цикл (без создания перевёрнутой строки)

function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-zа-яё0-9]/g, '');
const len = cleaned.length;

for (let i = 0; i < len / 2; i++) {
if (cleaned[i] !== cleaned[len - 1 - i]) {
return false;
}
}

return true;
}

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

СпособВремяПамятьЧитаемость
Переворот строкиO(n)O(n)Высокая
Два указателяO(n)O(1)*Высокая
РекурсияO(n)O(n) — стекСредняя
ЦиклO(n)O(1)*Высокая

*Без учёта памяти на очищенную строку.

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

Вопрос 11. Создайте компонент списка контактов на React с полями ввода имени и телефона, кнопкой добавления и возможностью удаления контактов.

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

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

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

Полная реализация компонента списка контактов на React с использованием функциональных компонентов и хуков.

Полный код компонента:

import React, { useState } from 'react';

function ContactList() {
// Состояния для полей ввода
const [name, setName] = useState('');
const [phone, setPhone] = useState('');

// Состояние для списка контактов
const [contacts, setContacts] = useState([]);

// Обработчик добавления контакта
function handleSubmit(event) {
event.preventDefault();

// Валидация: не добавляем пустые контакты
if (!name.trim() || !phone.trim()) {
return;
}

const newContact = {
id: Date.now(), // уникальный идентификатор
name: name.trim(),
phone: phone.trim(),
};

setContacts(function(prevContacts) {
return [...prevContacts, newContact];
});

// Очистка полей после добавления
setName('');
setPhone('');
}

// Обработчик удаления контакта
function handleDelete(id) {
setContacts(function(prevContacts) {
return prevContacts.filter(function(contact) {
return contact.id !== id;
});
});
}

return (
<div className="contact-list">
<h2>Список контактов</h2>

{/* Форма добавления */}
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Имя"
value={name}
onChange={function(event) {
setName(event.target.value);
}}
/>
<input
type="tel"
placeholder="Телефон"
value={phone}
onChange={function(event) {
setPhone(event.target.value);
}}
/>
<button type="submit">Добавить</button>
</form>

{/* Список контактов */}
{contacts.length === 0 ? (
<p>Контактов пока нет</p>
) : (
<ul>
{contacts.map(function(contact) {
return (
<li key={contact.id}>
<span>{contact.name}</span>
<span>{contact.phone}</span>
<button
onClick={function() {
handleDelete(contact.id);
}}
>
Удалить
</button>
</li>
);
})}
</ul>
)}
</div>
);
}

export default ContactList;

Ключевые моменты реализации:

1. Управляемые компоненты (Controlled Components)

Поля ввода привязаны к состоянию через value и onChange. Это стандартный подход в React, при котором состояние является «единственным источником истины» для значений полей.

2. Уникальные ключи

Для каждого контакта генерируется уникальный id через Date.now(). Это важно для корректной работы алгоритма согласования (reconciliation) React. Использование индекса массива в качестве ключа — плохая практика, так как при удалении элемента индексы сдвигаются, и React может некорректно обновлять DOM.

3. Удаление через filter

Метод filter создаёт новый массив, исключая контакт с указанным id. Это соответствует принципу иммутабельности — мы не мутируем существующий массив, а создаём новый.

4. Валидация

Перед добавлением проверяется, что оба поля заполнены (после удаления пробелов через trim()), чтобы избежать пустых контактов.

5. Очистка полей

После успешного добавления поля ввода сбрасываются к пустым строкам.

Альтернативная реализация с использованием единого состояния для формы:

function ContactList() {
const [form, setForm] = useState({ name: '', phone: '' });
const [contacts, setContacts] = useState([]);

function handleChange(event) {
const { name, value } = event.target;
setForm(function(prev) {
return { ...prev, [name]: value };
});
}

function handleSubmit(event) {
event.preventDefault();
if (!form.name.trim() || !form.phone.trim()) return;

setContacts(function(prev) {
return [...prev, { id: Date.now(), name: form.name.trim(), phone: form.phone.trim() }];
});

setForm({ name: '', phone: '' });
}

function handleDelete(id) {
setContacts(function(prev) {
return prev.filter(function(c) { return c.id !== id; });
});
}

return (
<div>
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="Имя" />
<input name="phone" value={form.phone} onChange={handleChange} placeholder="Телефон" />
<button type="submit">Добавить</button>
</form>
<ul>
{contacts.map(function(contact) {
return (
<li key={contact.id}>
{contact.name}{contact.phone}
<button onClick={function() { handleDelete(contact.id); }}>Удалить</button>
</li>
);
})}
</ul>
</div>
);
}

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

Вопрос 12. Что такое useEffect в React и для чего он используется?

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

Ответ собеседника: Правильный. useEffect — это хук React, который позволяет зацепиться за состояние и выполнять функции при выполнении каких-то условий, например при первом рендере или изменении состояний. Кандидат объяснил, что сайд-эффекты — это процессы, происходящие вне управления React, например, отправка запросов на сервер или логирование.

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

useEffect — это хук, который позволяет выполнять побочные эффекты (side effects) в функциональных компонентах React. Он заменяет методы жизненного цикла componentDidMount, componentDidUpdate и componentWillUnmount из классовых компонентов.

Синтаксис:

useEffect(function() {
// Код побочного эффекта

return function() {
// Функция очистки (cleanup)
};
}, [dependencies]);

Три варианта использования:

1. Выполнение при каждом рендере (без зависимостей):

useEffect(function() {
console.log('Компонент отрендерен');
});

2. Выполнение только при монтировании (пустой массив зависимостей):

useEffect(function() {
console.log('Компонент смонтирован');

return function() {
console.log('Компонент размонтирован');
};
}, []);

3. Выполнение при изменении определённых значений:

useEffect(function() {
console.log('userId изменился:', userId);
}, [userId]);

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

Загрузка данных с сервера:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(function() {
let isCancelled = false; // флаг для предотвращения утечек

setLoading(true);

fetch(`https://api.example.com/users/${userId}`)
.then(function(response) { return response.json(); })
.then(function(data) {
if (!isCancelled) {
setUser(data);
setLoading(false);
}
})
.catch(function(error) {
if (!isCancelled) {
setLoading(false);
}
});

return function() {
isCancelled = true; // отменяем обновление при размонтировании
};
}, [userId]);

if (loading) return <p>Загрузка...</p>;
if (!user) return <p>Пользователь не найден</p>;

return <h1>{user.name}</h1>;
}

Подписка на событие с очисткой:

useEffect(function() {
function handleResize() {
console.log('Размер окна:', window.innerWidth);
}

window.addEventListener('resize', handleResize);

return function() {
window.removeEventListener('resize', handleResize);
};
}, []);

Таймер с очисткой:

useEffect(function() {
const intervalId = setInterval(function() {
console.log('Тик');
}, 1000);

return function() {
clearInterval(intervalId);
};
}, []);

Важные правила:

  • Функция очистки (cleanup) вызывается перед следующим выполнением эффекта и при размонтировании компонента.
  • Массив зависимостей должен содержать все значения из замыкания, которые могут изменяться (props, state, переменные).
  • Без массива зависимостей эффект выполняется после каждого рендера.
  • Пустой массив [] означает, что эффект выполнится только один раз при монтировании.
  • Нельзя использовать useEffect внутри условий или циклов — хуки должны вызываться в одном и том же порядке при каждом рендере.