На лайвкодинге просят собрать небольшой компонент: поиск с подсказками, модалку, таймер, список задач, пагинацию. Проверяют не знание API, а то, как ты думаешь: состояние, крайние случаи, доступность, гонки.
План за две минуты
Прежде чем писать код, проговорите вслух:
- Что на входе и выходе: пропсы, колбэки, что видит пользователь.
- Минимальное состояние: только то, что нельзя вычислить. Отфильтрованный список не хранят в состоянии, его считают из исходного списка и запроса.
- Крайние случаи: пусто, загрузка, ошибка, очень длинный текст, быстрые повторные действия.
- Доступность: настоящие
button,labelу полей, управление с клавиатуры.
Интервьюер ценит этот план больше, чем скорость печати.
Производное состояние вместо синхронизации
// плохо: два источника правды и эффект-синхронизатор
const [filtered, setFiltered] = useState([]);
useEffect(() => setFiltered(items.filter((i) => i.done)), [items]);
// хорошо: считаем при рендере
const filtered = items.filter((i) => i.done);
Эффект здесь даёт лишний рендер и риск рассинхрона. Если вычисление дорогое, useMemo (или React Compiler).
Поиск с запросами: гонка ответов
Классика: пользователь быстро печатает «re», «rea», «react». Ответ на «re» может прийти последним и перетереть правильный результат. Решение: игнорировать устаревшие ответы или отменять запрос.
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) { setResults([]); return; }
const controller = new AbortController();
const timer = setTimeout(() => {
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch((e) => { if (e.name !== 'AbortError') console.error(e); });
}, 300);
return () => { clearTimeout(timer); controller.abort(); };
}, [query]);
return (
<>
<label htmlFor="q">Поиск</label>
<input id="q" value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
</>
);
}
Очистка эффекта убирает таймер (это и есть debounce) и отменяет старый запрос. Не забудьте encodeURIComponent. В реальном проекте этот код заменяет TanStack Query, но на собесе просят написать руками.
Модалка
Что ожидают увидеть:
- рендер через
createPortal(content, document.body), чтобыoverflow: hiddenиz-indexродителей не обрезали окно; - закрытие по Escape и по клику на подложку, но не по клику внутри окна (проверка
e.target === e.currentTarget); - фокус переходит в окно при открытии и возвращается на кнопку при закрытии;
role="dialog",aria-modal="true", заголовок черезaria-labelledby.
Нативный <dialog> с showModal() даёт ловушку фокуса, Escape и верхний слой без портала. Упомянуть его на собесе уместно.
Списки и key
Ключ должен быть стабильным и уникальным среди соседей: id из данных. Индекс массива как ключ ломает состояние элементов при удалении из середины или сортировке: React сопоставит состояние не тому элементу. Math.random() в ключе ещё хуже: каждый рендер пересоздаёт все элементы.
Таймеры и интервалы
useEffect(() => {
if (!running) return;
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, [running]);
Функциональное обновление s => s + 1 избавляет от устаревшего значения в замыкании, а очистка не даёт интервалам накапливаться.
Частые ошибки на лайвкодинге
- Хранить в состоянии то, что вычисляется.
- Забыть очистку эффекта: утечки таймеров и подписок.
- Не обработать загрузку и ошибку.
divсonClickвместо кнопки.- Молча писать код 20 минут, не проговаривая решения.
Что спросят на собесе
- Как избежать гонки ответов в поиске с подсказками?
- Как сделать доступную модалку и зачем портал?
- Почему не стоит использовать индекс массива как
key? - Что хранить в состоянии, а что вычислять при рендере?