Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

React: хуки, стан і рендеринг

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

useState і useEffect, ref і контекст, мемоізація, ключі й повторні рендери, конкурентний рендеринг, межі помилок і ліниве завантаження - питання всіх рівнів, від junior до senior.

За спробу
20
У пулі
97
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

60 питань

setCount(count + 1) не змінює змінну count - він планує новий рендер, у якому useState поверне нове значення. У поточному виконанні функції count лишається тим, яким був на початку рендеру. Стан - це «знімок» для конкретного рендеру.

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    console.log(count); // 0 - ще старий знімок
  }
  // після кліку count стане 1, а не 2
}

Обидва виклики порахували 0 + 1.

Функціональне оновлення - передати функцію, яка отримає актуальне значення з черги оновлень:

setCount((c) => c + 1);
setCount((c) => c + 1); // тепер 2

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

Ще правила стану:

  • Не змінювати стан на місці: items.push(x); setItems(items) - React порівнює посилання, бачить той самий масив і може не перерендерити. Правильно: setItems([...items, x]).
  • React групує оновлення (batching): кілька setState в одному обробнику дають один рендер.

Докладніше в документації: useState

Два правила:

  1. Хуки викликають лише на верхньому рівні компонента чи власного хука: не в умовах, циклах, вкладених функціях, після раннього return.
  2. Хуки викликають лише з функціональних компонентів і власних хуків, не зі звичайних функцій чи класів.

Чому: React не знає імен ваших станів. Він зберігає стан компонента як список і зіставляє виклики хуків з елементами списку за порядком: перший useState - перший елемент, другий - другий. Якщо на одному рендері виклик пропустили через умову, усі наступні хуки «з'їдуть» на чужі значення.

// Неправильно
if (isLoggedIn) {
  const [name, setName] = useState('');
}
const [theme, setTheme] = useState('light'); // візьме стан name, якщо умова змінилася

// Правильно: умова всередині, хук - завжди
const [name, setName] = useState('');
const displayName = isLoggedIn ? name : 'Гість';

Як не помилитися: плагін eslint-plugin-react-hooks перевіряє обидва правила й залежності useEffect. Його варто тримати увімкненим завжди.

Виняток: хук use (React 19) можна викликати в умовах і циклах - він читає Promise чи контекст і не зберігає стан у списку.

Докладніше в документації: Правила хуків

useEffect - для синхронізації компонента з зовнішньою системою: підписка на WebSocket, таймер, сторонній віджет, ручна робота з DOM, запит даних.

useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect();   // очищення
}, [roomId]);                              // залежності

Залежності: ефект перезапускається, коли змінилося будь-яке значення з масиву. В масиві мають бути всі реактивні значення, які ефект використовує, - лінтер react-hooks/exhaustive-deps за цим стежить. Порожній масив - лише після першого монтування.

Очищення виконується перед наступним запуском ефекту і при демонтуванні. Без нього - витоки: подвійні підписки, таймери, що живуть після компонента. У режимі розробки React навмисно монтує компонент двічі (StrictMode), щоб відсутність очищення стала помітною.

Коли ефект НЕ потрібен - найчастіша помилка:

  • Похідні дані. Не useEffect(() => setFullName(first + ' ' + last), [first, last]), а просто const fullName = first + ' ' + last під час рендеру. Дороге обчислення - useMemo.
  • Реакція на дію користувача. Відправка форми, аналітика кліку - в обробнику події, а не в ефекті, що стежить за станом.
  • Скидання стану при зміні props - через key на компоненті.

Запити даних в ефекті потребують обробки гонок (прапорець ignore в очищенні чи AbortController). У реальних застосунках для цього використовують TanStack Query, SWR або засоби фреймворку.

Докладніше в документації: Можливо, вам не потрібен ефект

useRef(initial) повертає об'єкт { current }, який живе весь час існування компонента. На відміну від стану, зміна ref.current не викликає рендер.

Два застосування:

  1. Доступ до DOM-елемента:
function Search() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Шукати</button>
    </>
  );
}
  1. Значення, яке треба зберігати між рендерами, але не показувати: ID таймера, попереднє значення, прапорець «чи вже відправлено», екземпляр сторонньої бібліотеки.
const timerRef = useRef(null);

function start() {
  timerRef.current = setInterval(tick, 1000);
}

function stop() {
  clearInterval(timerRef.current);
}

Стан чи ref:

  • Значення впливає на те, що показано, - стан.
  • Значення потрібне лише логіці й не має перемальовувати компонент - ref.

Правила:

  • Не читати й не записувати ref.current під час рендеру (крім лінивої ініціалізації) - рендер має бути чистим, а зміна ref не відображається в UI.
  • Для передачі ref у власний компонент у React 19 ref - звичайний prop. Раніше для цього потрібен був forwardRef.

Докладніше в документації: useRef

Власний хук - функція з назвою на use, що викликає інші хуки. Спосіб винести логіку зі станом і ефектами з компонента й перевикористати.

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query);
  const results = useSearch(debouncedQuery);
  // ...
}

Ключове: хуки ділять логіку, а не стан. Кожен компонент, що викликає useDebouncedValue, отримує свій незалежний стан. Для спільного стану потрібен контекст чи стор.

Ознаки, що варто виділити хук:

  • той самий набір useState + useEffect повторюється в кількох компонентах;
  • ефект синхронізується з чимось зовнішнім (useOnlineStatus, useMediaQuery, useWebSocket) - хук ховає деталі за зрозумілою назвою;
  • компонент став довгим, і логіка заважає читати розмітку.

Чого уникати:

  • Хуків-обгорток життєвого циклу (useMount, useUpdateEffect): вони ховають залежності від лінтера й заохочують мислення класовими компонентами.
  • Хуків, що лише групують useState без поведінки - виграшу немає.
  • Нестабільних результатів: функції, які хук повертає й які передаватимуть у залежності ефектів, обгортають у useCallback, інакше ефекти споживачів перезапускатимуться на кожен рендер.

Тестують хуки через renderHook з React Testing Library.

Докладніше в документації: Перевикористання логіки через власні хуки

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.