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 не викликає рендер.
Два застосування:
- Доступ до DOM-елемента:
function Search() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Шукати</button>
</>
);
}
- Значення, яке треба зберігати між рендерами, але не показувати: 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.
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, ідеї якого побудовані на тих самих редукторах.
Обидва хуки мають однаковий 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, що вставляють стилі до будь-яких вимірювань.
Проблема. Ефект має перезапускатися, коли змінюються одні значення, але використовує й інші, через які перезапускатися не повинен.
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 робить те саме офіційно й безпечніше для конкурентного рендерингу.