
Ниже - пример того, как я обычно представляю (и детально разбираю) один из моих любимых вопросов по фронтенд-разработке на собеседовании. Он основан на моем опыте интервьюирования в крупных 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.
Удачи на ваших будущих собеседованиях!
Комментарии (36)
dark_gf
03.02.2025 13:44Вопрос в общем хороший, у меня больше инетресует вот эта часть:
<Profile username="john_doe"> {(user) => (user === null ? <Loading /> : <Badge info={user} />)} </Profile>
Видя такой код я могу предположить что в проекте вашем может быть такого очень много, что имхо есть намек к не очень хорошему проекту )))
SergeyEgorov
03.02.2025 13:44Тоже хотел спросить- Это весь код компонента App, или что-то не показано с какой-то целью?
andry36 Автор
03.02.2025 13:44Да, в статье показан только упрощённый фрагмент компонента
App
. Он служит примером того, как использовать<Profile>
на практике - без лишнего кода, не относящегося к сути задачи.
andry36 Автор
03.02.2025 13:44Благодарю за мнение.
Этот пример с функцией вchildren
демонстрирует, что это обычный React-проп, которому можно передавать функцию как значение.
Сама идея упрощена для собеседования (в статье это указано), чтобы оценить именно понимание жизненного цикла и эффектов - в боевом проекте, конечно, принято грамотно разделять ответственность и не плодить запросы во всех мелких компонентах.
Zukomux
03.02.2025 13:44Простите, но запрос данных в эффекте это моветон. Если нужна загрузка данных, то ее надо оформлять хуком и делать функциональную композицию на уровне "умного" компонента(контроллера)
Xao
03.02.2025 13:44А уже хук будет вызывать внутри useEffect? Давайте угадаю, на выходе хотим три стейта - loading, error и result?
Начиная с React 19, этот подход устарел окончательно, лучше сделать запрос, получить промис и передать его дальше потребителям, используя Suspense и новый метод use. А ошибку ловить в ErrorBoundary.
js2me
03.02.2025 13:44Больше возникает вопрос - почему хранение бизнес логики приложения в слое предоставления правильное решение и почему команда React тоже так считает?
Почему это считается правильным только во фронтенд веба ?)
Vitaly_js
03.02.2025 13:44Из документации по реакт:
Only Suspense-enabled data sources will activate the Suspense component. They include:
Data fetching with Suspense-enabled frameworks like Relay and Next.js
Lazy-loading component code with
lazy
Reading the value of a cached Promise with
use
Suspense does not detect when data is fetched inside an Effect or event handler.
The exact way you would load data in the
Albums
component above depends on your framework. If you use a Suspense-enabled framework, you’ll find the details in its data fetching documentation.Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Что имеется в виду в последнем абзаце?
andry36 Автор
03.02.2025 13:44В последнем абзаце речь идёт о том, что полноценная реализация Suspense для произвольных запросов (без специальных фреймворков, типа Relay или Next.js) всё ещё считается нестабильной API. Другими словами, официальной документации и гарантий для общего случая пока нет, и React-команда может менять поведение или интерфейсы в будущем. Поэтому, если вы хотите «чистый» Suspense без дополнительных библиотек, стоит учесть, что это пока не является полностью поддерживаемым сценарием, и ваши решения могут сломаться в следующих версиях React.
Vitaly_js
03.02.2025 13:44О чем-то таком я тоже догадался. Мой вопрос спровоцировало вот это предложение: "Начиная с React 19, этот подход устарел окончательно". На мой взгляд, что бы что-то устарело окончательно должен накопиться опыт, и этот опыт должен перейти в эффективные решения. Данное же предложение из доки подсказывает, что это далеко не так.
Zukomux
03.02.2025 13:44И в итоге получаем компонент-мультитул. Тут и загрузка данных, и обработка ошибок, мапинг входных данных, формирование представления. Это подойдёт разве что для пет проекта, а в контексте статьи для проверки на джуна. Поменяйте роут с данными и придется 80% компонента переписать заново. Про тестирование я уже молчу
andry36 Автор
03.02.2025 13:44Хук внутри себя всё равно будет использовать
useEffect
для запроса данных, так что это вопрос именно композиции и структурирования кода, а не принципиального отличия в подходе. Пример был упрощен оставляя пространство для вопроса "А как бы вы сделали это?" :)Vitaly_js
03.02.2025 13:44Не обязательно же. Можно сделать через useSyncExternalStore. Тогда все эти задачи, которые вы проверяете в данном примере с монтирование/размонтирование, состоянием гонки и т.п. будут решены внутри объекта Profile и реакт тут вообще будет как бы ни при чем. Для использования надо только знать спецификацию useSyncExternalStore.
radist2s
03.02.2025 13:44Пожалуй, вы забыли про пару важных моментов: Abort Controller и React Strict Mode.
andry36 Автор
03.02.2025 13:44В статье я специально упомянул, что у нас якобы нет возможности вызвать
abort()
, чтобы проверить реакцию кандидата на ситуацию неуправляемого запроса. А Strict Mode помогает увидеть возможные побочные эффекты и дважды вызываемые функции - это тоже важно учитывать для корректной реализации асинхронной логики.radist2s
03.02.2025 13:44Просто обожаю технические задачи в вакууме на собеседованиях. Ваше идеальное решение не проходит даже базовую проверку на предотвращение рефетча (откройте консоль, и увидите как запрос выполняется дважды при первом маунте).
Меня всегда поражала подобная самоуверенность интервьюеров.
username-ka
03.02.2025 13:44Прикольный вопрос, я бы не принял ни предложенное решение, ни сам факт существования подобного компонента. Весь мир использует tanstack-query в качестве стандарта де-факто, камон.
andry36 Автор
03.02.2025 13:44Спасибо за комент!
"Весь мир использует..." громкое утверджение, сразу хочется попросить пруфы :)
Не все компании применяют именно tanstack-query. К тому же в задаче проверяются общие принципы работы с асинхронностью в React: если человек хорошо понимает эту логику, он без труда освоит и любую абстракцию вроде React Query или SWR.username-ka
03.02.2025 13:44У всех разные стандарты, это нормально. В моей реальности в 2025 году всё-таки стыдно было бы рисовать спиннеры руками "если данные null", тригерить загрузку в
useEffect
, игнорировать серверные компоненты и существование хуков."Не все компании применяют именно tanstack-query" - это действительно так. Но всерьёз кандидатов, который не задаёт первым вопросом "а нам точно надо такой велосипед напилить", я на своих интервью не рассматриваю.
onets
03.02.2025 13:44Есть callback hell, а это nano-micro-component hell, когда каждый уважающий себя микро-компонент лезет на сервер за данными. Когда их становится много - начинается подобное веселье.
andry36 Автор
03.02.2025 13:44Пример из статьи не призывает каждый микро-компонент делать собственный запрос. Это лишь иллюстрация ключевых вопросов, которые помогают оценить понимание React-хуков и потенциальных ловушек асинхронности. Для боевого применения, конечно, нужно распределять логику так, чтобы не превратить приложение в зоопарк из сотен запросов. Но на собеседовании хочется увидеть, как кандидат работает именно с базовой механикой React - тогда уже понятно, сможет ли он грамотно её применить внутри более продуманной архитектуры.
Vitaly_js
03.02.2025 13:44Представьте, что вы разрабатываете библиотеку компонентов внутри большой компании, и этой библиотекой будут пользоваться другие команды (вполне реальный сценарий в больших IT-компаниях). Задача: написать реализацию компонента
Profile
, чтобы им удобно и ожидаемо могли пользоваться в самых разных контекстах.Не очень люблю такое, потому что это смахивает на чисто синтетическую задачу или банальное запудривание мозгов соискателю.
Вот вы сами пишете, что уже естьfetchProfile
Тогда зачем нужен вот этот вот компонент? Реэкспортируйте вы этотfetchProfile
и тогда действительно его можно использовать в разных контекстах. И внутри реакт компонентов, и внутри какой-нибудь rtkquery. По сути, вся задача - это прослойка между пользовательским кодом иfetchProfile
, которая уже откидывает часть контекстов в которых будет использована.Вы действительно проверяете знания реакта, но, на мой взгляд, в неестественных условиях, т.е. создаете дополнительно пространство для стресса и для траты времени на муру.
andry36 Автор
03.02.2025 13:44Спасибо за комментарий!
Понимаю, что такая постановка задачи может показаться искусственной, но для собеседований она довольно типична: нам нужно в ограниченное время проверить, как кандидат мыслит в контексте React, понимает ли он тонкости хуков и асинхронных запросов.
На интервью часто задают подобные микро-задачи, чтобы быстро увидеть, как разработчик работает со стейтом, эффектами и обработкой ошибок на практике - без вдавания в детали полной архитектуры приложения.Т.е. что это не замена реального проектного кода, а тестовое окружение, где проявляются навыки кандидата.
Vitaly_js
03.02.2025 13:44Так в этом и проблема. Ваша задача быстро проверить навыки. А задача соискателя не ударить в грязь лицом. Если вы даете липовую задачу, действительно, можно не думая начать решать и вроде как показывать навыки. А можно начать рефлексировать и пытаться выяснить, а что тут проверяют?
Создавая искусственные ограничения вы не "упрощаете" задачу, а наоборот создаете реальные препятствия для ее решения. И как выше заметили можете создавать неверное представление о проекте.
fetchProfile
не позволяет отменять запросы. А fetchData1, fetchData2 тоже не позволяют? Как должен ответить соискатель? Решить задачу слепо заткнув дыру в архитектуре или поставить под сомнение реализациюfetchProfile
? В любом случае это игра ва-банк. Ты можешь показать себя либо безынициативным сразу взявшись за работу, либо наоборот излишне инициативным, либо неуверенным, либо болтливым и все это ровно из-за "липовости" тестового примера.И да, это действительно типично для собеседований, поэтому я и говорю, что такое не очень люблю. Вроде как хотят протестировать навыки (точно хотите навыки протестировать?)), вроде как сами же думают, что речь идет о реальных условиях, но тестируются далеко не только навыки и далеко не в реальных условиях.
Ione1991
03.02.2025 13:44Не совсем понятен первый пример с
lastUsernameFetched. Разве там условие не будет всегда ложно?
aw350me
03.02.2025 13:44Боже, как же все это противно выглядит.
Я все понимаю и про плюсы реакта и джаваскрипта в целом, у каждого языка есть свои недостатки, но конкретно этот пример показывает насколько же банальная работа с запросами это вязкая, противная, неоднозначная и нагроможденная работа в вебе с джс и реактом конкретно.
Сильно отталкивает от изучения и даже просто наблюдения за, казалось бы, перспективными и популярными разработками в сфере разработки веб приложений
questpc
03.02.2025 13:44Это просто им движет желание сделать "слишком хорошо" или идеально. По факту же консоль браузера в поисках ошибок обычный пользователь смотреть не будет. И непонятно почему сервер вернет null. Если пользователь отсутствует, то сервер вероятно вернет не 200 код вообще и ошибка будет на уровне запроса.
Частое тыканье в имена пользователей вообще лучше всего решить загрузкой данных для всех отображаемых в данный момент. Ну или Profile создавать не одну инстанцию а на каждый клик отдельно.
Kergan88
03.02.2025 13:44В интервью я обязательно спрашиваю: "А что, если проп username может динамически меняться? Например, пользователь кликает по спис
В этих случаях есть стандартное рекомендованное командой реакта решение - использовать ключи. Т.е.:
<Profile key={username} username={username}>
В итоге username внутри компонента меняться не будет. В этом случае нам не надо писать "мусорный" код, вместо этого гарантия корректности будет обеспечена на уровне фреймворка.
Дальше я описываю сценарий: представьте, что в вашем приложении две панели. Слева - список пользователей, справа - . Пользователь начинает быстро кликать то по одному, то по другому пользователю.
Поскольку реакт гарантирует корректность, об этом думать не надо, и ни чего по этому поводу делать не надо. Все будет работать правильно само по себе.
"Да, будет", ведь компонент может быть размонтирован, а асинхронный вызов вернётся. Возникает сценарий, когда React ругается - “Can’t perform a React state update on an unmounted component…”.
Нет, не будет. Это не является какой-либо проблемой. Корректность работы компонента уже гарантирована, и поэтому можно не думать о подобных вещах.
Идеальное решение
А теперь правильное идеальное решение:
const Profile = ({ username, children }) => { const [user, setUser] = useState(null); useEffect(() => fetchProfile(username).then(setUser), []); return children(user); };
Чтобы обеспечить тот же интерфейс, который требуется изначально, можно объявить дополнительный компонент:
const ProfileWrapper = ({ username, children }) => ( <Profile key={username} username={username}> {children} </Profile> );
Код работает корректно, согласно требованиям, ни каких проблем не имеет.
ЗЫ: а при использовании suspense компонент будет и вовсе выглядеть так:
const Profile = ({ username, children }) => children(use(fetchProfile(username)));
с соответствующей оберткой. И, что характерно - все будет работать как надо. Без ненужных изъебов. KISS.
UnknownHero
03.02.2025 13:441. Key решает проблему и будет показывать только последнее состояние и не важно что там вызывалось до этого.
2. По "Can’t perform a React state update on an unmounted component…” https://github.com/reactwg/react-18/discussions/82
3. Dependency тоже условный для сценария с key, можно не писать если код стайл позволяет.
Ох уж эти собесы с задачками.
walkwithmeinhell
03.02.2025 13:44Объясните плиз, как работает идеальное решение, если юзер быстро кликает по рандомным юзернеймам. С useref понял как люди предлагали. В идеальном не понимаю как и зачем тот setUser(null) в начале useeffect
Alex_Diamond
Подскажите, пожалуйста, пару интересных вопросов для сеньоров по этой задаче.
andry36 Автор
Спасибо, что обратили внимание на задачу!
Я бы задал вопросы, связанные с производительностью и оптимизацией, например:
Как организовать параллельную загрузку нескольких профилей и синхронизировать их результат в одном компоненте?
Как выстроить отказоустойчивость при сбоях сети (ретраи, отмена запросов, механизмы кэширования)?
Как решать проблему при работе в среде SSR, где запросы нужно делать ещё до рендера?
Как совместить подход с Suspense и Error Boundaries для более реактивной UX-модели?