> Как реализовать React компонент с текстовым полем для запроса к API, отображением результатов, индикатором загрузки и обработкой ошибок? (React)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Т-Банк, IT-One, Сбер

Стек: React

> Пример ответа

Короткий ответ

Реализуем controlled input с debounce, хук useEffect для отправки запроса при изменении значения, AbortController для отмены предыдущих запросов и useReducer для управления состояниями (loading, error, data). Индикатор загрузки показываем через спиннер, ошибки - через отдельный блок с возможностью повторного запроса. Для оптимизации используем useCallback и useMemo.

Подробное объяснение

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

  • Controlled input - значение хранится в state, onChange обновляет его
  • Debounce - задержка перед отправкой запроса (300-500ms), реализуется через setTimeout в useEffect
  • AbortController - отменяет предыдущий запрос при новом вводе, предотвращая устаревшие ответы
  • useReducer - управляет состояниями: idle, loading, success, error. Это чище, чем несколько useState
  • Обработка ошибок - catch блок с установкой error state, отображение сообщения и кнопки retry
  • Отмена при unmount - cleanup функция в useEffect отменяет запрос и таймер

Для production стоит добавить кэширование результатов (через Map или React Query), обработку пустого результата и доступность (aria-live для screen readers).

На практике

В реальных проектах часто используют готовые решения: React Query (TanStack Query) или SWR, которые предоставляют встроенные механизмы кэширования, ревалидации и дедупликации запросов. Но для понимания основ важно уметь реализовать вручную.

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

  • Использовать useRef для хранения AbortController, чтобы не создавать лишние ререндеры
  • Не забывать очищать таймер debounce при размонтировании
  • Обрабатывать ошибки сети отдельно от ошибок API (статус 4xx vs 5xx)
  • Показывать скелетон или спиннер, но не блокировать интерфейс
  • Для списка результатов использовать виртуализацию (react-window) при большом количестве

Пример кода

JSX
import { useState, useEffect, useReducer, useRef, useCallback } from 'react';
const initialState = {
status: 'idle',
data: [],
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'LOADING':
return { ...state, status: 'loading', error: null };
case 'SUCCESS':
return { status: 'success', data: action.payload, error: null };
case 'ERROR':
return { status: 'error', data: [], error: action.payload };
case 'RESET':
return initialState;
default:
return state;
}
}
function SearchComponent() {
const [query, setQuery] = useState('');
const [state, dispatch] = useReducer(reducer, initialState);
const abortRef = useRef(null);
const timerRef = useRef(null);
const searchApi = useCallback(async (searchTerm, signal) => {
const response = await fetch(`/api/search?q=${searchTerm}`, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}, []);
useEffect(() => {
if (!query.trim()) {
dispatch({ type: 'RESET' });
return;
}
// Отмена предыдущего запроса
if (abortRef.current) abortRef.current.abort();
const controller = new AbortController();
abortRef.current = controller;
// Debounce
clearTimeout(timerRef.current);
timerRef.current = setTimeout(async () => {
dispatch({ type: 'LOADING' });
try {
const results = await searchApi(query, controller.signal);
dispatch({ type: 'SUCCESS', payload: results });
} catch (err) {
if (err.name !== 'AbortError') {
dispatch({ type: 'ERROR', payload: err.message });
}
}
}, 300);
return () => {
clearTimeout(timerRef.current);
controller.abort();
};
}, [query, searchApi]);
const handleRetry = () => {
setQuery(prev => prev + ' '); // триггер повторного запроса
};
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск..."
aria-label="Поиск"
/>
{state.status === 'loading' && <div>Загрузка...</div>}
{state.status === 'error' && (
<div role="alert">
Ошибка: {state.error}
<button onClick={handleRetry}>Повторить</button>
</div>
)}
{state.status === 'success' && (
<ul>
{state.data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}

Как отвечать на собеседовании

Начни с ключевых проблем: race condition, debounce, управление состояниями. Объясни, почему используешь useReducer вместо нескольких useState - это показывает понимание управления сложным состоянием. Упомяни AbortController как решение для отмены запросов. Если спросят про оптимизацию - расскажи про мемоизацию callback'ов и значений, виртуализацию списка.

Для senior позиции важно показать понимание edge cases: что будет при быстром вводе, при размонтировании компонента, при ошибках сети. Хорошо добавить про accessibility (aria-live, role="alert") и про то, как это масштабируется на несколько полей поиска на странице.

Что проверяет интервьюер

  • Понимание жизненного цикла React компонентов и эффектов
  • Умение работать с асинхронностью и отменой запросов
  • Знание паттернов управления состоянием (useReducer vs useState)
  • Внимание к деталям: очистка таймеров, обработка ошибок, предотвращение утечек памяти
  • Способность проектировать компоненты с учетом UX (индикаторы загрузки, обработка ошибок)

Типичные ошибки

  • Не используют AbortController - при быстром вводе могут отобразиться устаревшие результаты
  • Забывают очищать таймер debounce при unmount - вызывает setState на размонтированном компоненте
  • Хранят результаты в одном useState с загрузкой и ошибкой - приводит к сложной логике обновления
  • Не обрабатывают ошибки сети отдельно от ошибок API
  • Используют setTimeout без cleanup функции в useEffect
  • Не учитывают пустой ввод - отправляют запросы с пустой строкой

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью