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

Middle: питання на співбесіді з теми «Хуки»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

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

useReducer виносить логіку оновлення стану в окрему функцію-редуктор: компонент лише описує, що сталося (дію), а редуктор вирішує, як змінюється стан.

function cartReducer(state, action) {
  switch (action.type) {
    case 'added':
      return { ...state, items: [...state.items, action.item] };
    case 'removed':
      return { ...state, items: state.items.filter((i) => i.id !== action.id) };
    case 'cleared':
      return { ...state, items: [] };
    default:
      throw new Error(`Невідома дія: ${action.type}`);
  }
}

function Cart() {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  return <button onClick={() => dispatch({ type: 'cleared' })}>Очистити</button>;
}

Коли useReducer кращий:

  • кілька пов'язаних значень змінюються разом (статус запиту, дані, помилка; кроки майстра);
  • багато різних оновлень одного стану розкидано по обробниках - редуктор збирає їх в одному місці;
  • наступний стан залежить від попереднього складним чином;
  • тестування: редуктор - чиста функція, її тестують без React: expect(cartReducer(state, action)).toEqual(...);
  • передача вниз: dispatch стабільний між рендерами - його безпечно передавати в дочірні компоненти й контекст без useCallback.

Коли досить useState: незалежні прості значення (відкрито/закрито, текст у полі).

Правила редуктора:

  • чистий: без запитів, таймерів, випадкових значень - React може викликати його двічі в StrictMode;
  • не мутує стан - повертає новий об'єкт;
  • дії описують подію ('added', 'login_failed'), а не «встановити поле» - тоді логіка зосереджена в редукторі, а не в компонентах.

Лінива ініціалізація - третій аргумент: useReducer(reducer, userId, createInitialState) - функція викличеться лише один раз.

Масштабування: useReducer + Context - простий спосіб керувати станом частини застосунку без зовнішніх бібліотек. Для складнішого - Redux Toolkit, ідеї якого побудовані на тих самих редукторах.

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

Обидва хуки мають однаковий API, різниця - коли вони виконуються відносно відмальовування екрана.

  • useEffect - після того, як браузер відмалював зміни. Не блокує показ - підходить для більшості ефектів (запити, підписки, логування);
  • useLayoutEffect - після оновлення DOM, але до відмальовування. Браузер чекає, поки ефект виконається.

Коли потрібен useLayoutEffect: коли треба виміряти DOM і одразу на основі вимірювання змінити розмітку - щоб користувач не побачив проміжного стану.

Класичний приклад - підказка (tooltip), яка має з'явитися над елементом, а якщо зверху немає місця - під ним:

function Tooltip({ targetRect, children }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    setTooltipHeight(ref.current.getBoundingClientRect().height);
  }, []);

  const top = targetRect.top - tooltipHeight < 0
    ? targetRect.bottom
    : targetRect.top - tooltipHeight;

  return <div ref={ref} style={{ position: 'fixed', top }}>{children}</div>;
}

З useEffect користувач на мить побачив би підказку не на тому місці, а потім вона «стрибнула» б. З useLayoutEffect вимірювання й перерахунок відбуваються до першого показу.

Інші випадки: відновлення позиції прокрутки, анімації, що стартують від виміряної позиції, синхронізація зі сторонніми бібліотеками, що вимірюють DOM.

Ціна: useLayoutEffect блокує відмальовування. Важка робота в ньому робить інтерфейс повільнішим. За замовчуванням - useEffect, а useLayoutEffect - лише коли видно «мерехтіння».

Серверний рендер: на сервері немає DOM і розкладки - ні useEffect, ні useLayoutEffect там не виконуються. Компонент, що залежить від вимірювань, на сервері рендериться без них; варіанти - показувати його лише на клієнті або мати розумний вигляд за замовчуванням.

useInsertionEffect - ще раніший хук, лише для бібліотек CSS-in-JS, що вставляють стилі до будь-яких вимірювань.

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

Проблема. Ефект має перезапускатися, коли змінюються одні значення, але використовує й інші, через які перезапускатися не повинен.

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', () => {
      showNotification('Підключено', theme);   // потрібна поточна тема
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);   // зміна теми перепідключає до чату!
}

Прибрати theme із залежностей не можна - буде застаріле значення, і лінтер це помітить. Додати - зайві перепідключення.

useEffectEvent (стабільний з React 19.2) виносить нереактивну частину логіки в «подію ефекту»: функцію, яка завжди бачить актуальні props і стан, але не є залежністю:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Підключено', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', () => onConnected());
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);   // лише справжня причина перепідключення
}

Як розрізняти:

  • реактивна логіка (має перезапускати синхронізацію) - у тілі ефекту, її значення в залежностях;
  • логіка-реакція (виконується в певний момент і має бачити свіжі значення) - у useEffectEvent.

Інші типові випадки: аналітика з поточними даними користувача при зміні сторінки, обробник інтервалу, що читає свіжий стан, колбек з props (onChange), який батько щоразу створює заново.

Обмеження:

  • викликати лише з ефектів - не з обробників подій і не під час рендеру;
  • не передавати в інші компоненти й хуки - функція оголошується поруч з ефектом, що її використовує;
  • це не спосіб «приглушити» лінтер: якщо значення справді має перезапускати ефект, воно лишається в залежностях.

До появи хука це розв'язували через useRef з останнім значенням колбеку - useEffectEvent робить те саме офіційно й безпечніше для конкурентного рендерингу.

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