Питання на співбесіді: Хуки
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
15 питань
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в одному обробнику дають один рендер.
Два правила:
- Хуки викликають лише на верхньому рівні компонента чи власного хука: не в умовах, циклах, вкладених функціях, після раннього
return. - Хуки викликають лише з функціональних компонентів і власних хуків, не зі звичайних функцій чи класів.
Чому: 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 чи контекст і не зберігає стан у списку.
React вирішує, чи перерендерити компонент, порівнюючи посилання: новий стан має бути новим об'єктом. Зміна старого об'єкта «на місці» не помітна для React - і ламає логіку порівняння в memo, useEffect, useMemo.
Об'єкти - копія з розгортанням:
const [user, setUser] = useState({ name: 'Оля', address: { city: 'Київ', street: '' } });
// неправильно: мутація
user.address.city = 'Львів';
setUser(user);
// правильно: нові об'єкти на кожному рівні шляху до зміни
setUser({
...user,
address: { ...user.address, city: 'Львів' },
});
Розгортання (...) поверхневе - копіювати треба кожен рівень, що змінюється.
Масиви - методи, що повертають новий масив:
| Дія | Замість мутації | Без мутації |
|---|---|---|
| додати | push, unshift |
[...items, item] |
| видалити | splice |
items.filter((i) => i.id !== id) |
| змінити | items[i] = x |
items.map((i) => (i.id === id ? { ...i, done: true } : i)) |
| вставити | splice |
[...items.slice(0, n), item, ...items.slice(n)] |
| сортувати | sort, reverse |
items.toSorted(...), items.toReversed() |
Пастка зі «скопійованим» масивом: [...items] копіює масив, але елементи-об'єкти лишаються ті самі. copy[0].done = true змінює об'єкт і в старому стані.
Глибоко вкладений стан робить такі оновлення громіздкими. Два виходи:
- Immer (
use-immer): пишете «мутацію», а бібліотека створює новий незмінний об'єкт:
updateUser((draft) => {
draft.address.city = 'Львів';
});
- сплощити стан - зберігати сутності за
idв окремих полях замість глибокої вкладеності.
Функціональне оновлення для залежності від попереднього стану: setItems((prev) => [...prev, item]).
Чому це важливо не лише для рендеру: незмінність дає «знімки» історії (скасування дій), дешеве порівняння для оптимізацій і передбачувані ефекти.
useEffect синхронізує компонент із зовнішньою системою: підписка, таймер, WebSocket, сторонній віджет, запит до API.
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect(); // очищення
}, [roomId]);
Масив залежностей визначає, коли ефект перезапускається:
| Варіант | Коли виконується |
|---|---|
| без масиву | після кожного рендеру |
[] |
один раз після монтування (і очищення при розмонтуванні) |
[roomId] |
після монтування і щоразу, коли roomId змінився |
Значення порівнюються через Object.is: для об'єктів і функцій, створених під час рендеру, кожен рендер - «нове» значення, і ефект перезапускається щоразу.
Функція очищення виконується:
- перед кожним повторним запуском ефекту - зі старими значеннями (відключитися від старої кімнати перед підключенням до нової);
- при розмонтуванні компонента.
Без очищення - витоки: таймери продовжують працювати, підписки накопичуються, відповіді на старі запити перезаписують нові дані.
Залежності не обирають - їх перелічують. Кожне реактивне значення (props, стан, змінні й функції з тіла компонента), що використовується в ефекті, має бути в масиві. Правило лінтера react-hooks/exhaustive-deps перевіряє це автоматично. Якщо ефект перезапускається надто часто, рішення - змінити код (винести функцію всередину ефекту, функціональне оновлення стану, useEffectEvent), а не викинути залежність з масиву.
StrictMode в режимі розробки монтує компонент двічі: ефект → очищення → ефект. Це навмисна перевірка, що очищення написане правильно. Якщо від цього щось ламається (подвійний запит із побічним ефектом, подвійна аналітика), - бракує очищення.
Коли ефект не потрібен: обчислення з props чи стану (рахувати під час рендеру), реакція на дію користувача (робити в обробнику події), скидання стану при зміні props (через key).
Для доступності елементи форм зв'язують через id: <label htmlFor>, aria-describedby, aria-labelledby. Але компонент поля може бути на сторінці кілька разів - жорстко прописаний id="email" дублюватиметься.
useId генерує унікальний ідентифікатор для кожного екземпляра компонента:
import { useId } from 'react';
function PasswordField({ label, hint }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type="password" aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>{hint}</p>
</>
);
}
Один виклик - базовий id, а пов'язані ідентифікатори утворюють з суфіксами.
Чому не Math.random() чи лічильник:
- серверний рендер: HTML генерується на сервері, потім React «гідратує» його в браузері.
Math.random()дасть на клієнті інший id, ніж на сервері, - помилка розбіжності гідратації, а зв'язкиlabel-inputзламаються; - кожен рендер - новий id, якщо генерувати в тілі компонента, і атрибути постійно змінюватимуться;
- глобальний лічильник залежить від порядку рендеру, який на сервері й клієнті може відрізнятися.
useId будує ідентифікатор зі шляху компонента в дереві - він однаковий на сервері й клієнті і стабільний між рендерами.
Чого useId не робить:
- не для
keyу списках - ключі мають походити з даних (item.id); - не для пошуку елементів через
document.getElementById- для доступу до DOM єuseRef; - не гарантує конкретного формату - не будуйте на ньому CSS-селектори.
Кілька застосунків React на одній сторінці (наприклад, «острівці» на серверній сторінці) можуть згенерувати однакові id. Для них задається префікс: createRoot(el, { identifierPrefix: 'search-' }) і такий самий на сервері.
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 робить те саме офіційно й безпечніше для конкурентного рендерингу.
Власний хук - функція з назвою на 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.
Докладніше в документації: Перевикористання логіки через власні хуки
Context передає значення від провайдера до будь-якого нащадка без props на кожному рівні: тема, мова, поточний користувач, налаштування.
const ThemeContext = createContext('light');
<ThemeContext value={theme}> {/* React 19; раніше ThemeContext.Provider */}
<App />
</ThemeContext>
const theme = useContext(ThemeContext);
Чому всі споживачі перерендерюються: коли значення провайдера змінилося (порівняння через Object.is), React перерендерює кожен компонент, що читає цей контекст, навіть якщо використовує лише частину значення. Вибіркової підписки на поле в контексту немає.
Типова пастка - новий об'єкт на кожен рендер:
<AuthContext value={{ user, login, logout }}>
Кожен рендер провайдера створює новий об'єкт - і всі споживачі перерендерюються, навіть якщо user не змінився.
Як з цим жити:
- Мемоізувати значення:
const value = useMemo(() => ({ user, login, logout }), [user]), функції - черезuseCallback. - Розділяти контексти: дані, що часто змінюються, окремо від рідкісних; стан окремо від функцій-диспетчерів (
StateContextіDispatchContext). - Не тримати в контексті часто змінюваний стан великої частини застосунку (значення полів форми, позиція курсору). Для такого краще стор з селекторами (Zustand, Redux, Jotai), де компонент підписується лише на потрібну частину.
Context - не менеджер стану, а механізм передачі. Він чудово працює для рідко змінюваних значень і погано масштабується для глобального стану, що часто оновлюється.
Докладніше в документації: Передача даних глибоко через Context
Більшість стану живе в React (useState, useReducer). Але інколи дані зберігаються зовні: у сторонній бібліотеці стану, в API браузера (navigator.onLine, matchMedia, localStorage), у власному класі-сховищі.
Наївний підхід - useEffect з підпискою і копією в useState - має проблеми в конкурентному рендерингу: React може рендерити частини дерева в різний час, і різні компоненти побачать різні версії зовнішнього значення («розрив», tearing).
useSyncExternalStore - офіційний спосіб читати зовнішнє джерело узгоджено:
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // знімок на клієнті
() => true, // знімок для серверного рендеру
);
}
Три аргументи:
subscribe(callback)- підписатися на зміни й повернути функцію відписки;getSnapshot()- поточне значення;getServerSnapshot()- значення для серверного рендеру й гідратації (на сервері немаєnavigator).
Ключові правила:
getSnapshotмає повертати те саме значення, доки дані не змінилися. Якщо він щоразу створює новий об'єкт (() => ({ ...store.state })), React вважатиме, що дані змінилися на кожному рендері, - нескінченний цикл і помилка. Повертайте збережене незмінне значення або примітив;subscribe- стабільна функція, оголошена поза компонентом (чи вuseCallback), інакше React перепідписуватиметься на кожному рендері;- оновлення з такого джерела не можна позначити як transition - вони завжди синхронні.
Хто вже використовує: Redux (useSelector), Zustand, TanStack Query та інші бібліотеки стану побудовані на ньому. У застосунку цей хук зазвичай потрібен для API браузера чи власного сховища.
Приклад із localStorage: підписка на подію storage (зміни з інших вкладок) плюс власна подія для змін у поточній вкладці - і стан синхронізований між вкладками.
use(resource) читає значення з проміса або контексту. На відміну від хуків, його можна викликати в умовах і циклах.
1. Читання проміса з Suspense:
import { use, Suspense } from 'react';
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // «призупиняє» компонент до виконання проміса
return comments.map((c) => <p key={c.id}>{c.text}</p>);
}
function Post({ commentsPromise }) {
return (
<Suspense fallback={<p>Завантаження коментарів...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
Поки проміс виконується, показується fallback найближчого <Suspense>. Відхилений проміс потрапляє в найближчу межу помилок (error boundary).
Головна пастка - звідки береться проміс. Проміс, створений під час рендеру клієнтського компонента (use(fetch('/api/comments'))), буде новим на кожному рендері - React показуватиме fallback знову й знову. Проміс має бути стабільним:
- створений у серверному компоненті й переданий клієнтському як prop (основний сценарій у Next.js);
- закешований бібліотекою (TanStack Query, роутер з завантажувачами даних);
- створений поза рендером (в обробнику події, в завантажувачі маршруту).
2. Читання контексту:
function Toolbar({ showTheme }) {
if (showTheme) {
const theme = use(ThemeContext); // useContext тут заборонений правилами хуків
return <ThemeBadge theme={theme} />;
}
return null;
}
Що відрізняє use від хуків:
- можна викликати умовно і в циклах;
- але лише в компонентах і хуках - не в звичайних функціях і не в
try/catch(помилки обробляє error boundary, а неcatch); - не створює власного стану.
У серверних компонентах use для промісів не потрібен - там можна просто await. use - для клієнтських компонентів, що отримують проміс.
Чим це краще за useEffect для даних: немає проміжного стану «ще не завантажено» в кожному компоненті, немає гонитви запитів у ефектах, а стани завантаження й помилки декларативно задаються межами Suspense і error boundary.
Кожен рендер компонента - окремий виклик функції зі своїми значеннями props і стану. Функції, створені під час рендеру (обробники, колбеки ефектів), «запам'ятовують» значення цього рендеру. Якщо функція живе довше за рендер, вона бачить старі значення - це застаріле замикання (stale closure).
Класичний приклад - інтервал:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count завжди 0 - замикання першого рендеру
}, 1000);
return () => clearInterval(id);
}, []); // ефект створено один раз
return <p>{count}</p>; // зупиняється на 1
}
Способи виправити:
1. Функціональне оновлення - не читати стан у замиканні взагалі:
setCount((c) => c + 1);
Найкращий варіант, коли нове значення залежить лише від попереднього.
2. Додати значення в залежності - ефект перезапуститься з новим замиканням:
useEffect(() => { /* ... */ }, [count]);
Коректно, але для інтервалу означає перестворення таймера щосекунди.
3. useEffectEvent - логіка, що має бачити свіжі значення, але не перезапускати ефект:
const onTick = useEffectEvent(() => setCount(count + step));
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, []);
4. useRef з поточним значенням - старий спосіб (ref оновлюється на кожному рендері, колбек читає ref.current). Працює, але useEffectEvent робить це явніше.
Де ще трапляється:
- обробники сторонніх бібліотек, зареєстровані один раз (карта, WebSocket);
useCallbackз неповними залежностями - мемоізована функція бачить старий стан;- асинхронні функції: після
awaitзначення змінних - ті, що були на момент виклику, навіть якщо стан уже змінився.
Профілактика: правило лінтера react-hooks/exhaustive-deps знаходить більшість застарілих замикань у ефектах і мемоізації. Попередження «missing dependency» - майже завжди справжня помилка, а не надмірна прискіпливість лінтера.