Одна React-задача, демонстрирующая ключевые навыки на собеседовании

Ниже — пример того, как я обычно представляю (и детально разбираю) один из моих любимых вопросов по фронтенд-разработке на собеседовании. Он основан на моем опыте интервьюирования в крупных IT-компаниях. Этот вопрос посвящён созданию небольшого React-компонента, который асинхронно получает данные на основе пропса username
. Он кажется простым, но на самом деле показывает много нюансов понимания кандидатом хуков React, сайд-эффектов, состояния гонки (race conditions) и компромиссов в дизайне. Приятного чтения!
Как и у любых других вопросов для собеседования, у этого есть недостатки. Собеседование — искусственная ситуация с жёсткими временными ограничениями, и кандидат может нервничать или уставать. Моя цель — не поймать человека на ошибке, а понять, как он рассуждает о реальных проблемах, с которыми может столкнуться в работе.
Суть задачи на интервью
Перед вами один из возможных примеров использования компоненты <Profile>, который получает проп
username
и внутри делает запрос на некий API (например, fetchProfile(username)) – это некая абстракция: может быть GitHub, может быть корпоративный сервис, без разницы.
const App = () => {
return (
<Profile username="john_doe">
{(user) => (user === null ? <Loading /> : <Badge info={user} />)}
</Profile>
);
};
Представьте, что вы разрабатываете библиотеку компонентов внутри большой компании, и этой библиотекой будут пользоваться другие команды (вполне реальный сценарий в больших IT-компаниях). Задача: написать реализацию компонента
Profile
, чтобы им удобно и ожидаемо могли пользоваться в самых разных контекстах.
import React, { useState, useEffect, useRef } from 'react';
import fetchProfile from 'somewhere';
// Это фиктивная функция, которая возвращает Promise,
// резолвящийся в объект пользователя
function Profile() {
// Допишите здесь логику
}
Важный дисклеймер:
-
user === null ? <Loading /> : <Badge info={user} />
— это упрощённая проверка. В реальном мире сервер может вернутьnull
в ответ, и нам придётся делать дополнительную логику, чтобы корректно обрабатывать “нет данных” vs. “данные ещё загружаются”. Однако мы намеренно оставим такой код, чтобы посмотреть, заметит ли кандидат потенциальную проблему и предложит ли более надёжное решение (например,isLoading
флаг). -
Наш
fetchProfile
не даёт возможности вызватьabort()
. Это сделано специально, чтобы проверить, знает ли кандидат про аборт запросов (AbortController
) и как он будет рассуждать, если такой возможности нет.
Начинаем решение
Чаще всего кандидаты сначала пишут что-нибудь простое, используя функциональные компоненты и хуки:
import React, { useState, useEffect } from 'react';
import fetchProfile from 'profileApi'; // воображаемый модуль
const Profile = ({ username, children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchProfile(username).then(setUser);
}, []);
return children(user);
};
Как ни странно, но на этом этапе у многих возникают сложности с пониманием того, что в children
может быть функция, и её можно просто вызвать: children(user)
, даже не оборачивая во всякие <div>...</div>
или <></>
. Почему-то кандидатам с ними спокойнее.
Однако уже тут видно несколько типичных моментов:
-
Отсутствие зависимостей в
useEffect
.Часто люди забывают добавитьusername
в массив зависимостей. Это значит, что еслиusername
поменяется, запрос на новый профиль не произойдёт. -
Необработанные ошибки. А что если
fetchProfile
завершится ошибкой или вернётnull
?
Уточняем детали
В интервью я обязательно спрашиваю: «А что, если проп username
может динамически меняться? Например, пользователь кликает по списку пользователей?» Тогда кандидат обычно исправляет код, добавляя username
в зависимости эффекта:
useEffect(() => {
fetchProfile(username).then(setUser);
}, [username]);
Теперь, если username
меняется, мы делаем новый запрос. Так понятнее. Но…
Race condition (гонка состояний)
Дальше я описываю сценарий: представьте, что в вашем приложении две панели. Слева — список пользователей, справа — <Profile username={currentUsername} />
. Пользователь начинает быстро кликать то по одному, то по другому пользователю.
-
Запрос A уходит для
username = 'alice'
. -
Тут же пользователь кликает на
username = 'bob'
, отправляется запрос B. -
Запрос B возвращается быстрее, мы записываем в state данные
bob
. -
Потом запрос A (более медленный) тоже возвращается, и внезапно перезаписывает state данными пользователя Alice!
«Может быть тут какая либо проблема?». К счастью в основном ответ да — при таком кейсе у нас может отображаться неправильная информация. На экране написано “bob”, а по факту в компоненте данные “alice”.
Разбор типичных решений
Приведу несколько реальных подходов, которые я видел от кандидатов. Самые экзотические — типа очереди запросов — опустим :)
Локальная переменная вне компонента
Иногда пытаются сделать что-то вроде:
let lastUsernameFetched = null;
function Profile({ username, children }) {
const [user, setUser] = useState(null);
lastUsernameFetched = username;
useEffect(() => {
fetchProfileManaged(username).then((profile) => {
if (lastUsernameFetched !== username) {
setUser(profile);
}
});
}, [username]);
return children(user);
}
По сути, мы храним состояние (lastUsernameFetched
) на уровне модуля. Но что, если на странице несколько экземпляров <Profile>
? Придётся как-то разделять их по идентификаторам. Это далеко не лучшее решение…
Использование useRef для отслеживания текущего username
Иногда кандидаты придумывают хранить текущий username
в useRef
, чтобы при получении результата сравнивать, совпадает ли он со значением пропса. Кандидат начинает спрашивать про структуру ответа, и в этом месте мы обычно вводим предположение, что username
в объекте профиля всё-таки есть. В результате вижу такое решение:
const Profile = ({ username, children }) => {
const [user, setUser] = useState(null);
const usernameRef = useRef(username);
useEffect(() => {
fetchProfile(username).then((profile) => {
if (usernameRef.current === profile?.username) {
setUser(profile);
}
});
}, [username]);
return children(user);
};
Почему-то часто встречал заблуждение, что useRef(username)
всегда будет передавать в usernameRef
актуальное значение пропса (хотя на самом деле это лишь начальное значение). После выяснения этого обстоятельства встречаются исправления в виде:
...
useEffect(() => {
usernameRef.current = username;
}, [username]);
...
Это приводит к лишнему вызову эффекта, но чаще встречается, к счастью, такой ответ:
const Profile = ({ username, children }) => {
const [user, setUser] = useState(null);
const usernameRef = useRef(username);
useEffect(() => {
usernameRef.current = username;
fetchProfile(username).then((profile) => {
if (usernameRef.current === profile?.username) {
setUser(profile);
}
});
}, [username]);
return children(user);
};
Отлично, идем дальше.
А если у нас в приложении две страницы, и пользователь уходит со страницы с
<Profile>
раньше, чем придёт ответ отfetchProfile
будет ли тут какая-либо проблема?
«Да, будет», ведь компонент может быть размонтирован, а асинхронный вызов вернётся. Возникает сценарий, когда React ругается — “Can’t perform a React state update on an unmounted component…”.
Тогда нередко вижу такой решение:
...
useEffect(() => {
return () => {
usernameRef.current = null;
}
}, []);
...
Это, как правило, вовсе не гарантирует, что setUser
никогда не будет вызван (мало ли, если не хороший сервер вернёт null
).
Идеальное решение
Часто самый простой подход (при отсутствии AbortController
) — завести внутри useEffect
переменную-флаг:
const Profile = ({ username, children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
let isLive = true;
setUser(null);
fetchProfile(username)
.then((profile) => {
if (isLive) {
setUser(profile);
}
})
.catch((err) => {
// Здесь можно обсудить дополнительные аспекты обработки ошибок.
// Если интересно, какие именно - пишите вопросы к статье :)
});
return () => {
isLive = false;
};
}, [username]);
return children(user);
};
-
Пока
isLive = true
, состояние обновляется при поступлении ответа -
Если компонент размонтировался или
username
изменился (а значит, эффект сработает заново), переменнаяisLive
сбрасывается вfalse
. В результате старый запрос, вернувшийся с задержкой, не изменит состояние. -
Таким образом, удаётся избежать гонки при обновлении состояния и предупредить возникновение ошибки в React при вызове
setState
на размонтированном компоненте. -
Добавление блока
catch
наглядно показывает возможность обработки ошибок от сервера или сети. При необходимости можно обсудить способы уведомления пользователя и логирования таких ошибок.
Примечание: для упрощения здесь не рассматривается сценарий, когда
username
илиchildren
могут оказаться «пустыми» (например,null
,undefined
или пустая строка), а также ситуация, когдаchildren
не является функцией. Однако здорово, если кандидат обратит внимание и на эти нюансы.
Почему мне нравится этот вопрос
Он небольшой по объёму и наглядно показывает ключевые аспекты работы с React: получение данных, состояние загрузки, корректный рендер и работу с пропами.
Он проверяет базовые знания React: хуки, сайд-эффекты, “cleanup” при размонтировании, изменение пропсов со временем — всё это ключевые концепции во фронтенд-разработке на React.
Он выявляет важные крайние случаи:
-
Проп
username
может меняться, пока запрос ещё выполняется. -
При уходе со страницы до завершения запроса может случиться попытка обновить state размонтированного компонента.
-
Сервер может вернуть
null
или ошибку. -
Может возникнуть состояние гонки при быстрых переключениях пользователя.
Его можно масштабировать. Джуны могут представить простую рабочую версию, а для синьоров я могу задать дополнительные вопросы про оптимизацию, отмену запросов, работу с несколькими запросами одновременно.
Итог
Моя цель в подобных React-вопросах — не просто услышать готовое решение, а понять, как человек рассуждает:
-
Задаёт ли он уточняющие вопросы: “Что если
username
меняется?”, “Что если у нас много быстрых кликов?”, “Нужна ли отмена запроса?” -
Понимает ли он асинхронные эффекты и их подводные камни?
-
Учитывает ли он необходимость освободить ресурсы при размонтировании компонента?
-
Думает ли о загрузке / ошибках / логировании — ведь сервер может вернуть
null
, ошибку, или просто долго висеть.
В конце концов, главное — это структура размышлений. Точно так же, как в системном дизайне мы обсуждаем компромиссы по сложности, памяти, пропускной способности, здесь в React-собеседовании смотрим на подход к работе с данными, пропами, асинхронностью, состоянием и реактивным UI.
Удачи на ваших будущих собеседованиях!
Автор: andry36