Міркування щодо React Hooks через знімки відображення та компроміси.
Продвинутий модель мислення для React та React Native Hooks: знімки відображення, ефекти, refs, мемоїзація, власні Hooks та способи пояснення компромісів під час співбесід.
Джерела: React & React Native Hooks: The Complete Senior Interview Guide
Інтерв’ю з кандидатами на високі посади з React та React Native рідко обмежуються питанням „Що робить цей Hook?“. Питання про useState вже за хвилину переходить до теми групових операцій та функціональних оновлень; питання про useEffect стає предметом обговорення нескінченних циклів, застарілих замикань, скасування операцій та ситуацій, коли ефект взагалі не повинен існувати; useMemo та useCallback безпосередньо приводять до питання „Чи справді це прискорює роботу?“. Запам’ятовані визначення не витримують таких подальших запитань. Те, що залишається, — це послідовна модель, і цей посібник будує саме її: спочатку рендеринг, потім стан, замикання, ефекти, refs, мемоізація, власні Hooks, продуктивність та нарешті архітектура, з прикладами React Native там, де платформа змінює ситуацію.
Модель рендерингу, від якої залежить усе інше
Почніть з однієї ідеї та тримайтеся її: коли React відображає компонент, він знову викликає функцію компонента з самого початку. Використовуйте найменший можливий лічильник.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Цілком природно уявляти, що натискання кнопки впливає на компонент та змінює змінну count. Але насправді цього не відбувається. Функція-змінник просить React про нове відображення, і наступний виклик Counter отримує нове значення як окрему константу.
Render 1
count = 0
↓
User clicks
↓
setCount(1)
↓
React schedules another render
↓
Render 2
count = 1
Кожен процес відображення має свою копію своїх параметрів та стану. Ніщо всередині цієї копії не змінюється пізніше. Це просте спостереження пояснює існування замикань на старих значеннях, застарілий стан у таймерах, причину наявності залежностей ефектів, поведінку керуючих функцій та обробників подій, а також те, чому мемоїзація пов’язана з ідентичністю. Інтерв’юери, які заглиблюються за межі API, зазвичай перевіряють, чи розумієте ви цю модель.
useState: стан, який належить процесу відображення
useState надає функційному компоненту шматок стану, який впливає на його результат.
const [count, setCount] = useState(0);
Пара, яку ви отримуєте назад, має дві різні функції: значення для поточного процесу відображення та функцію, яка запитує оновлення.
count
↓
current state
setCount
↓
request a state update
Виклик функції-встановлювача не змінює нічого на місці; він заплановує новий процес відображення, у якому компонент бачитиме нове значення.
Як поступово ускладнюються запитання
Типовий перебіг навчання виглядає так:
- Базовий рівень: яку проблему вирішує
useState? - Середній рівень: чим стан відрізняється від звичайної локальної змінної?
- Продвинутий рівень: коли функція-оновлювач є правильним вибором?
- Глибокий рівень: що насправді створює
setCount(count + 1), виконане тричі в одному обробнику?
Відповідь «Це зберігає стан» відповідає лише на перше запитання. Краща відповідь охоплює всю сутність:
useState зберігає значення, яке впливає на те, що відображає компонент. Виклик функції-змінника записує у чергу нове відображення з оновленим значенням, тому стан краще розглядати як частину «знімка» цього відображення, а не як змінну, яку можна змінювати. Коли наступне значення залежить від попереднього, функція-оновлювач є безпечним вибором, оскільки React групує оновлення, а зафіксоване значення вже може бути застарілим.
Натомість звичайна змінна створюється знову з її початковим значенням під час кожного оновлення, і зміна її ніколи не змушує React оновлюватися знову, тому вона не може керувати інтерфейсом.
Чому важливі функціональні оновлення
Ось три операції збільшення, виконані на основі зафіксованого значення.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
Усі три варіанти читають одне й те саме значення count з поточного знімка, тому після об’єднання результатом є одне збільшення, а не три. Формат оновлювача виправляє цю проблему:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
React передає кожному оновлювачу найсвіжіше значення, тож три виклики працюють правильно. Використовуйте цей формат, коли кілька оновлень відбуваються одночасно або чергуються, коли копі-функція може виконуватися зі старим знімком, а також загалом тоді, коли нове значення обчислюється зі старого.
Кошик покупок у додатку React Native є реалістичним прикладом. Кількість товарів відображається, тому вона має знаходитися у стані:
const [quantity, setQuantity] = useState(1);
Функції-обробники отримують наступне значення з попереднього, а функція зменшення обмежує його значення одиницею, щоб у кошику ніколи не було нуля чи від’ємної кількості.
const increaseQuantity = () => {
setQuantity(prev => prev + 1);
};const decreaseQuantity = () => {
setQuantity(prev => Math.max(1, prev - 1));
};
Нічого не змінюється між вебом та нативними платформами
Хук є ідентичним на обох платформах. У веб-версії спадний список може відстежувати, чи він відкритий:
const [isOpen, setIsOpen] = useState(false);
Екран у React Native також може відстежувати видимість модального вікна аналогічним чином:
const [isModalVisible, setModalVisible] = useState(false);
Механізм стану є спільним; відрізняються лише примітиви користувацького інтерфейсу, які він керує.
useEffect: синхронізація з зовнішнім світом
Жоден Hook не створює більше обговорень під час інтерв’ю. Поширений опис „він виконує код після рендерингу“ є правдивим, але не пояснює мету. Краще сформулювати це так: ефект забезпечує синхронізацію вашого компонента з системою, якою React не керує.
У веб-середовищі цією системою можуть бути події DOM, API браузера, WebSockets, підписки, таймери чи мережеві запити. У React Native це часто AppState, модуль Keyboard, нативні модулі, слухачі сповіщень, стан мережі, API пристрою, WebSockets та таймери.
Як поступово ускладнюються запитання
- Базовий рівень: для чого використовується
useEffect? - Середній рівень: поясніть, як працює масив залежностей.
- Високий рівень: що може змусити ефект працювати безкінечно?
- Високий рівень+: у яких ситуаціях ефект є неправильним інструментом?
- Рівень продакшену: як скасувати підписку на WebSocket?
Порівняйте твердження «useEffect призначений для викликів API» з цим:
useEffect існує переважно для того, щоб підтримувати синхронність компонента з зовнішньою системою. Масив залежностей містить список реактивних значень, які читає ефект, і він має бути точним; правило lint для Hooks виявляє пропуски. Коли ефект створює підписку, запускає таймер або приєднує слухач, він повертає функцію очищення, щоб старі ресурси були звільнені перед повторним виконанням ефекту або коли компонент демонтовується.
Читання масиву залежностей
Якщо є залежність, ефект виконується після першого відображення та знову щоразу, коли це значення змінюється між відображеннями.
useEffect(() => {
fetchUser(userId);
}, [userId]);
Якщо масиву зовсім немає, ефект виконується після кожного відображення:
useEffect(() => {
// ...
});
Якщо масив порожній, ефект виконується після монтажу компонента:
useEffect(() => {
// ...
}, []);
Будьте обережні з фразою „виконується один раз“. У режимі розробки Strict Mode навмисно монтує, очищує та знову монтує ефекти, щоб виявити ті, які не очищуються належним чином. Точна формулювання полягає у тому, що ефект з порожнім масивом виконується після монтажу, а режим Strict Mode у розробці може додати додатковий цикл налаштування та очищення. Код, який не працює під час цього подвійного виконання, зазвичай має справжню помилку.
Очищення запобігає витоку ресурсів
Підписка, яка залежить від userId, відкриває з’єднання у тілі ефекту:
useEffect(() => {
const subscription = subscribeToUpdates(userId);
і повертає функцію, яка його закриває:
return () => {
subscription.unsubscribe();
};
}, [userId]);
Коли змінюється userId, React виконує попереднє очищення перед запуском нового ефекту, тому завжди існує не більше однієї активної підписки:
Old effect
↓
cleanup old subscription
↓
new effect
↓
subscribe using new userId
Під час демонтажу виконується останнє очищення, і нічого не залишається позаду:
component unmounts
↓
cleanup
↓
subscription removed
Той самий механізм використовується для скасування асинхронних операцій. Для запиту fetch можна припинити виконання запиту за допомогою AbortController під час очищення, або встановити флаг ignore, щоб запізніла відповідь від старого userId не перезаписувала новіші дані.
Приклад у React Native: реакція на AppState
Більш переконливим ефектом, ніж отримання даних, є той, який пов’язує компонент із життєвим циклом нативного додатку. Цей слухач оновлює дані, коли додаток повертається на передній план, та видаляється під час зняття компонента.
useEffect(() => {
const subscription = AppState.addEventListener(
"change",
nextState => {
if (nextState === "active") {
refreshData();
}
}
);
return () => {
subscription.remove();
};
}, []);
Це синхронізація з зовнішньою системою у її найпростішій формі, і це показує співбесіднику, що ви розумієте призначення ефектів.
Помилка: синхронізація похідних значень через ефект
Припустимо, форма зберігає ім’я та прізвище:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
Поширеним антипаттерном є додавання третього елемента стану та ефекту для його оновлення:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Це призводить до зайвого стану та додаткової обробки кожного разу, коли змінюється ім’я, причому під час однієї обробки повне ім’я є застарілим. Оскільки значення повністю визначається існуючим станом, обчислюйте його під час обробки:
const fullName = `${firstName} ${lastName}`;
Принцип, який варто сказати вголос: якщо значення можна обчислити синхронно з пропсів або стану, отримуйте його під час обробки замість того, щоб зберігати його та оновлювати через ефект. Цей паттерн розглядається більш детально у статті про припинення синхронізації стану з useEffect.
Помилка: нестабільні залежності від об’єктів
Тут літераль об’єкта створюється всередині компонента та вказується як залежність.
const filters = {
status: "active",
};
useEffect(() => {
fetchUsers(filters);
}, [filters]);
Кожен рендер створює новий об’єкт filters, а залежності порівнюються за посиланням, тому ефект щоразу сприймає це як „зміну“. Якщо ефект оновлює стан, цей цикл сам себе підживлює:
Render
↓
new filters object
↓
dependency changed
↓
effect
↓
setState
↓
render
↓
new filters object
↓
effect again
Рефлексивним рішенням є обгортання об’єкта мемоізацією:
useMemo(...)
Перш ніж це робити, потрібно з’ясувати, чи взагалі об’єкт потрібно створювати всередині компонента. Константу можна розмістити поза компонентом; значення, створене з пропсів, часто можна замінити шляхом використання його примітивних полів (наприклад status) або шляхом його створення всередині ефекту. Виправлення потоку даних — це більш професійний підхід; додавання мемоізації для його приховування — ні.
useRef: пам’ять, яка не рендерується
useRef повертає змінний контейнер, який зберігається між рендерами, і чиї зміни ніколи не викликають рендерування.
const valueRef = useRef(0);
Ви читаєте та записуєте його вміст через властивість current:
valueRef.current
Присвоєння нового значення, як показано нижче, оновлює контейнер, але залишає екран у тому ж стані, доки щось інше не спричинить перерисовування.
valueRef.current = 10;
Інтерв’юери зазвичай переходять від питання «У чому різниця між ref та state?» до питання «Наведіть реальний приклад», а потім — до питання «Чому тут краще використовувати ref замість state?». Хороша відповідь:
State — це дані, які використовуються для перерисовування, тож їх зміна запускає процес перерисовування. Ref зберігає змінне значення між перерисовуваннями, не викликаючи перерисовування при зміні
.current. Ref підходять для імперативних об’єктів керування, ідентифікаторів таймерів, попередніх значень та інших змінних даних, які не відображаються на інтерфейсі.
У веб-середовищі ref забезпечує імперативний доступ до вузла DOM, наприклад для фокусування поля введення:
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
У React Native refs також є дуже поширеними, оскільки мобільні інтерфейси часто потребують імперативних дій, таких як прокручування списку назад до верхньої частини:
const scrollRef = useRef<ScrollView>(null);
scrollRef.current?.scrollTo({
y: 0,
animated: true,
});
Вибір між станом та ref
Вирішальне питання просте: чи потребує зміна цього значення зміни того, що бачить користувач? Якщо так, оберіть
useState
а якщо ні, оберіть
useRef
Активна вкладка, очевидно, впливає на інтерфейс, тому це є станом:
const [selectedTab, setSelectedTab] = useState("home");
Об’єкт таймера існує лише для того, щоб його можна було пізніше скасувати; ніщо не відображає його, тому ref є правильним варіантом:
const timerRef = useRef<number | null>(null);
useMemo: кешування обчисленого значення
useMemo зберігає результат обчислень та перераховує їх лише тоді, коли змінюється якась залежність. Класичним прикладом є фільтрований та відсортований список користувачів:
const filteredUsers = useMemo(() => {
return users
.filter(user => user.active)
.sort(sortUsers);
}, [users]);
Доки users зберігає ту саму посилання, фільтрація та сортування пропускаються під час наступних відображень. Зверніть увагу на приховану вимогу: sortUsers також має бути стабільним або вказаним як залежність, а функція sort змінює дані на місці, що тут є безпечним лише через те, що filter вже створив новий масив.
Наступне запитання майже завжди звучить так: «Чи варто використовувати його скрізь?». Відповідь, яка враховує критерії оцінки:
Ні. useMemo — це не безкоштовний інструмент: він додає відстеження залежностей, споживання пам’яті та обсяг коду для обробки. Він виправдовує себе тоді, коли дорогі обчислення повторюються без причини або коли стабільне посилання має значення для подальших етапів обробки, причому спочатку слід провести профілювання.
Надмірне використання Memoization для об’єднання рядків є класичним прикладом такого зловживання:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
Робота є тривіальною, тому простий вираз одночасно швидше для читання та принаймні так само швидко виконується:
const fullName = `${firstName} ${lastName}`;
Тепер розгляньмо додаток для торгівлі в React Native, який відображає тисячі угод, фільтровані за символом та часовим проміжком, та впорядковані за новизною.
const visibleTrades = useMemo(() => {
return trades
.filter(trade => trade.symbol === selectedSymbol)
.filter(trade => trade.timestamp >= startTime)
.sort((a, b) => b.timestamp - a.timestamp);
}, [trades, selectedSymbol, startTime]);
Коли ця трансформація стає значно повільною, мемоізація виявляється корисною. Навіть тоді переконайтеся в цьому за допомогою профілера, а не просто припускайте це.
useCallback: збереження ідентичності функції
useCallback повертає той самий об’єкт функції між переробками, поки не зміняться його залежності.
const handlePress = useCallback(() => {
submitOrder();
}, [submitOrder]);
Різниця з useMemo полягає лише у тому, що кешується. Один з них кешує значення:
useMemo
→ memoises a value
Інший — посилання на функцію:
useCallback
→ memoises a function reference
Насправді useCallback(fn, deps) поводиться так само, як useMemo(() => fn, deps). Відповідь „щоб покращити продуктивність“ є слабкою на запитання „чому його використовувати?“. Краща відповідь:
useCallback зберігає стабільність ідентичності функції, коли її залежності залишаються незмінними. Його варто використовувати там, де справді має значення рівність посилань, наприклад у калеці, яка передається мемоїзованому компоненту, або у функції, яку інший Hook вказує як залежність. Його не слід додавати за замовчуванням, оскільки мемоїзація має свою вартість та ускладнює розуміння коду.
Як це пов’язано з React.memo
React.memo пропускає перерендеринг компонента, якщо всі його пропси поверхнево є ідентичними попереднім.
const UserRow = React.memo(function UserRow({
user,
onPress,
}) {
return (
// ...
);
});
Якщо батьківський компонент передає нову функцію-стрілку щоразу під час рендерингу, один проп завжди буде відрізнятися:
<UserRow
user={user}
onPress={() => openUser(user.id)}
/>
Оскільки посилання змінюється, поверхневе порівняння провалюється, і все одно відображається запам’ятований рядок, що може приховано скасувати переваги React.memo. Стабільний обробник виглядає так:
const handlePress = useCallback(() => {
openUser(user.id);
}, [user.id]);
і передається замість функції безпосередньо в коді:
<UserRow
user={user}
onPress={handlePress}
/>
Пам’ятайте про застереження: робіть це тоді, коли у вас є докази того, що переробка дочірнього елемента відбувається з великими витратами, а не як автоматична реакція.
Замикання та проблема застарілих значень
Замикання — це функція JavaScript, а не Hook, проте без них складно розуміти Hooks. Замикання — це функція, яка зберігає доступ до змінних того області видимості, де вона була створена.
function createLogger(name: string) {
return () => {
console.log(name);
};
}
Внутрішня функція все ще бачить name навіть після того, як createLogger повернувся.
Як з’являється застаріле замикання в React
Розгляньмо компонент із лічильником:
function Counter() {
const [count, setCount] = useState(0);
та два кнопки: одна для збільшення кількості, а інша — для запису значення після односекундної затримки:
const handleClick = () => {
setTimeout(() => {
console.log(count);
}, 1000);
}; return (
<>
<button onClick={handleClick}>
Log Count
</button> <button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</>
);
}
Припустимо, що обробник запису був створений під час відображення:
count = 0
Якщо ви натиснете „Записати кількість“ та кілька разів збільшите її протягом цієї секунди, все одно виведеться повідомлення про затримку
0
навіть попри те, що наступні процеси відображення вже продовжилися
count = 1
count = 2
count = 3
Колбек затримки охоплює змінну count з того процесу відображення, де вона була створена, і цей „знімок“ ніколи не змінюється.
Пояснення під час співбесіди
Компоненти на основі функцій працюють у сніппетах відображення: кожне відображення має свої значення та власні замикання. Колбек, створений під час одного відображення, фіксує значення цього відображення, тож щось асинхронне, як-от таймер, може спостерігати за старим станом. Рішення залежить від потреби: функціональне оновлення, корекція залежностей ефектів або використання ref, коли справді потрібне останнє змінне значення без необхідності відображення.
У цій відповіді названо причину та запропоновано три рішення з умовами для кожного з них, що саме і шукає досвідчений інтерв’юер.
Custom Hooks: упакування поведінки
Custom Hooks — це місце, де поєднуються попередні ідеї. Custom Hook — це функція, назва якої починається з use, та яка викликає інші Hooks, що дозволяє виділити поведінку зі станом та використовувати її знову. Завантажувач профілів починає з оголошення свого стану:
function useUserProfile(userId: string) {
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
Потім виконується запит через ефект, прив’язаний до userId, і повертаються три значення:
useEffect(() => {
// fetch user
}, [userId]); return {
data,
loading,
error,
};
}
Екран використовує їх, не розуміючи, як відбувається завантаження:
function ProfileScreen({ userId }) {
const {
data,
loading,
error,
} = useUserProfile(userId);
і відображає все необхідне з отриманих результатів:
// UI
}
Зауважте, що кожен компонент, який викликає власний Hook, має свій незалежний стан; Hook ділиться логікою, а не даними.
Що насправді перевіряє інтерв’юер
Питання „Чи можете ви написати власний Hook?“ насправді стосується здатності розпізнати повторно використовувану абстракцію поведінки. Хороші кандидати виглядають ось так:
useAuth()
useUserProfile()
useNetworkStatus()
usePagination()
useDebounce()
useKeyboard()
useAppState()
usePermissions()
useInfiniteScroll()
У React Native прикладом є Hook статусу мережі – він підписується на зміни підключення, зберігає флаг у стані та повертає функцію видалення підписки як частину очищення.
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = subscribeToNetworkChanges(
state => {
setIsConnected(state.isConnected);
}
);
return unsubscribe;
}, []);
return {
isConnected,
};
}
Екран оплати потім приймає рішення на основі отриманого результату:
function CheckoutScreen() {
const { isConnected } = useNetworkStatus();
if (!isConnected) {
return <OfflineMessage />;
}
return <Checkout />;
}
Екран не має уявлення про те, як налаштовуються мережеві підписки, і саме ця незнання є суттю абстракції.
Інші інструменти з набору Hook
Під час співбесід з кандидатами на вищі посади часто розглядаються не лише п’ять поширених Hook, тож варто знати більш широкий набір.
useContext
Він зчитує значення, надане вище у ієрархії.
const theme = useContext(ThemeContext);
Він підходить для спільних, крос-функціональних проблем: теми, автентифікації, локалізації та налаштувань. Це не є бібліотекою для керування станом; кожен користувач переробляє відображення, коли змінюється надане значення, що має значення для частого оновлення даних.
useReducer
Коли кілька елементів стану змінюються одночасно, окремі функції-встановлювачі розсіюють логіку переходу:
setLoading(...)
setData(...)
setError(...)
Редюсер централізує її:
const [state, dispatch] = useReducer(reducer, initialState);
Компоненти описують те, що сталось, а редюсер визначає наступний стан:
dispatch({
type: "FETCH_SUCCESS",
payload: data,
});
Моделювання переходів як іменованих дій робить складні стани легшими для прогнозування та тестування.
useLayoutEffect
Він має таку саму структуру, як useEffect, але виконується синхронно після оновлення дерева React та перед малюванням браузером, що саме потрібно, коли необхідно виміряти розташування елементів та внести корективи до того, як користувач побачить затримку. У React Native існують подібні ситуації, де важливий час малювання. Це не швидший useEffect; оскільки він блокує процес малювання, його надмірне використання погіршує швидкість реакції системи.
useId
Він генерує стабільні унікальні ідентифікатори, переважно для забезпечення доступності, наприклад для поєднання етикетки з полем введення. Він добре підходить для роботи в Інтернеті; у React Native, де немає браузерного DOM, сфера його застосування обмежена.
useImperativeHandle
Це дозволяє компоненту самостійно вирішувати, що саме буде відкрито через приєднаний ref, замість того щоб викривати базовий вузол. Компонент, який можна використовувати багаторазово, може відкривати функції фокусування:
ref.current?.focus();
або прокрутки до верху:
ref.current?.scrollToTop();
Такий контрольований імперативний інтерфейс особливо корисний для спільних компонентів React Native.
useSyncExternalStore
Це підтримуваний спосіб читання даних з сховища, яке знаходиться поза React, що забезпечує послідовні копії навіть під час одночасного відображення. Це складний механізм, але він згадується під час обговорень архітектури та є основою для бібліотек стану.
useTransition
Він позначає деякі оновлення як невимогливі. Концептуально, вимогливі операції забезпечують реактивність інтерфейсу:
Urgent update
↓
Keep UI responsive
тоді як невимогливі операції можуть бути заплановані з нижчим пріоритетом:
Non-urgent update
↓
Can be scheduled with lower priority
Це має значення у великих додатках, де тривала переробка змісту конкурує з процесом введення тексту чи натискання елементів.
useDeferredValue
Він відкладає обробку значення, щоб швидкі взаємодії залишалися оперативними. Класичний сценарій:
User typing
↓
Keep input responsive
↓
Defer expensive filtering
Введений текст оновлюється миттєво, тоді як складна фільтрація обробляє відкладену копію запиту.
Правила хуків
Два правила забезпечують коректну роботу хуків, оскільки React ідентифікує кожен хук за порядком його виклику.
Викликайте хуки на верхньому рівні
Ніколи не обгортайте хук у умову, цикл чи оператор раннього повернення:
if (isLoggedIn) {
useEffect(() => {});
}
Натомість розмістіть умову всередину хука:
useEffect(() => {
if (!isLoggedIn) {
return;
}
і вкажіть значення, від якого він залежить:
// logic
}, [isLoggedIn]);
Викликайте хуки лише з функцій React
Хуки призначені для використання у функційних компонентах та користувацьких хуках, а не у довільних допоміжних функціях чи методах класів.
Про продуктивність
„Цей екран працює повільно, що б ви зробили?“ — це поширене запитання. Відповідь „додати useMemo та useCallback“ свідчить про неправильний підхід. Краща відповідь:
Першим кроком є з’ясування того, де насправді йде час: як часто відрендеровуються компоненти, які обчислення є витратними, де знаходяться межі компонентів, як відрендеровуються списки, як поширюються оновлення стану та яка платформа використовується. Потім потрібно проаналізувати ситуацію та усунути справжню проблему.
Продуктивність React Native — це більше, ніж просто відрендерування
Витрати мобільного екрана розподіляються між кількома шарами:
React rendering
↓
JavaScript execution
↓
Native platform work
↓
Layout
↓
Images
↓
Animations
↓
Lists
↓
Network
Тож коли екран у React Native працює з перешкодами, причиною може бути не
useState
або
useEffect
Це може бути пов’язано з великою кількістю роботи JavaScript, надмірною кількістю відображень, великими списками, обробкою зображень, завантаженням анімацій, операціями на рівні операційної системи, затримками в мережі чи підпискою на стани, які активують занадто багато компонентів. Здатність визначити, де шукати проблему серед усіх цих елементів, — ось що відрізняє досвідчених розробників мобільних додатків.
const visibleTransactions = transactions
.filter(...)
.sort(...)
.map(...);
Зберігання результатів фільтрації та сортування в пам’яті — це один з розумних кроків:
const visibleTransactions = useMemo(() => {
return transactions
.filter(...)
.sort(...);
}, [transactions]);
Це рідко є повною відповіддю. Вам також потрібна віртуалізація списків, ефективне відображення елементів, стабільні ключі, компоненти рядків з кешуванням, обмежені підписки на стани, розумне завантаження зображень та адаптивні взаємодії. У React Native правильний вибір FlatList або FlashList та їх належна налаштування зазвичай мають набагато більше значення, ніж один useMemo. Питання полягає у тому, де знаходиться вузьке місце, а не у тому, який Hook додати.
Власність на стани та архітектура
Context проти бібліотек зберігання даних
На запитання «Коли варто використовувати Context замість Redux чи Zustand?» немає однозначної відповіді.
Це залежить від стану. Контекст добре підходить для досить стабільних, універсальних значень, таких як тема чи конфігурація. Для складного стану додатку, який часто оновлюється та потребує селекторів, прогнозованих переходів чи детальної підписки, зазвичай краще використовувати спеціалізовану бібліотеку стану.
Важливими є концепції власності та частоти оновлення.
Не все має знаходитися у глобальному сховищі
Поширеною архітектурною помилкою є збирання всіх значень у одне сховище:
Everything
↓
Global store
Краще запитати, хто є власником кожної частини стану. Локальний стан компонента охоплює короткочасні деталі інтерфейсу:
isModalOpen
inputValue
selectedTab
Стан сервера — це дані, отримані з API; їх часто краще обробляти за допомогою спеціалізованої бібліотеки серверного стану, яка керує кешуванням та повторним отриманням даних:
user profile
products
orders
transactions
Глобальний стан клієнта — це невеликий набір значень, якими справді користуються багато екранів:
theme
authentication state
application preferences
У веб-середовищі частина стану має знаходитися у URL, щоб він зберігався після перезавантаження та міг бути поширений через посилання:
filters
search
pagination
Правило, яке потрібно пам’ятати: стан має знаходитися у найвужчому можливому діапазоні, який має сенс.
Застосування на екрані інвестицій
Уявіть екран інвестицій у React Native, який потребує даних ринку, обраного символа, даних графіка, даних портфеля, обробки AppState, прокручування та складної обробки графіка. Спрощена структура:
InvestmentScreen
│
├── useMarketData()
│
├── usePortfolio()
│
├── useAppState()
│
├── useRef()
│
├── useMemo()
│
└── useCallback()
│
▼
UI
Нижче наведено опис компонента. Слід звернути увагу на два моменти: фрагмент закінчується кількома рядками прози та розділовими знаками, які не є коректним кодом, тож його потрібно розглядати як псевдокод; крім того, функція setSymbol з модуля useState вже є стабільною, тому використання обгортки useCallback тут є необов’язковим, і можна безпосередньо передати сам встановлювач.
function InvestmentScreen() {
const [symbol, setSymbol] = useState("AAPL");
const scrollRef = useRef<ScrollView>(null);
const { data: marketData } = useMarketData(symbol);
const { portfolio } = usePortfolio();
const chartData = useMemo(() => {
return transformMarketData(marketData);
}, [marketData]);
const handleSymbolChange = useCallback(
(nextSymbol: string) => {
setSymbol(nextSymbol);
},
[]
);
return (
<ScrollView ref={scrollRef}>
<MarketHeader
symbol={symbol}
onSymbolChange={handleSymbolChange}
/>
<PriceChart data={chartData} />
<PortfolioSummary
portfolio={portfolio}
/>
</ScrollView>
);
}
---
Now each concept has a clear responsibility.
---
Кожен Hook тепер має одну, зрозумілу функцію:
useState
→ selected symboluseRef
→ imperative ScrollView accessuseMemo
→ expensive chart transformationuseCallback
→ stable handler where usefulCustom Hooks
→ market and portfolio behaviouruseEffect
→ internal synchronization within those Hooks
Ефекти залишаються всередині кастомних Hooks, де вони синхронізуються з потоком даних ринку та життєвим циклом додатка, а сам екран майже нагадує опис його даних.
Відповідь від старшого інженера
Вийти на один рівень за межі запитання
Поширеною помилкою є відповідь лише на те, що було запитано. На запитання „Що таке useMemo?“ відповідь „Воно зберігає значення в пам’яті“ є правильною, але легко забутною. Більш повна відповідь:
useMemo зберігає у кеші обчислений значення, ключоване за його залежностями. Він не є стандартним елементом, оскільки має додаткові витрати та ускладнює код. Він стає доцільним, коли дороге обчислення виконується зайвий раз або коли потрібен стабільний посилання, ідеально після того, як профілювання виявить проблемну точку.
За кілька речень ви продемонстрували знання API, усвідомлення продуктивності, знання компромісів та здатність до судження.
Структура відповіді з п’яти кроків
Надійний шаблон для будь-якого запитання щодо Hook:
1. What is it?
2. Why does it exist?
3. Real example
4. Trade-off / pitfall
5. React Native example when relevant
Застосовується до useRef:
- Що: постійне змінне значення, яке не спричиняє оновлення інтерфейсу.
- Чому: для даних, які мають залишатися незмінними після оновлень, але не є частиною UI.
- Приклад: ідентифікатор таймера або об’єкт керування імперативним компонентом.
- Компроміс: оскільки його оновлення не призводить до оновлення інтерфейсу, він не повинен містити стан UI.
Питання, до яких потрібно підготуватися
Щодо useState, будьте готові обговорювати функції оновлення та групування кількох викликів, чому зміна звичайної змінної не змінює екран, що спричиняє перерендерування, де має знаходитися певний елемент стану та як уникати стану, який насправді є похідним.
Щодо useEffect, очікуйте запитань про роль масиву залежностей, застарілі закриття функцій, багаторазове виконання, для чого потрібна функція очищення, скасування поточної асинхронної роботи, ситуації, коли effect є неправильним інструментом, та подвійне викликання у режимі Strict Mode під час розробки.
Щодо useRef, підготуйтеся пояснити, чому запис у .current не призводить до перерендерування, у яких ситуаціях ref кращий за стан, як refs прив’язуються до DOM чи нативних елементів та що означає імперативний API.
Щодо useMemo, потрібно чітко розуміти, чи він завжди прискорює роботу, як він відрізняється від useCallback, що означає референційна рівність, коли доцільно його видалити та як довести, що він допоміг.
Щодо useCallback, потрібно знати, чому ідентичність функції може мати значення, як вона поєднується з React.memo, коли вона не додає жодної користі, чи може вона уповільнити роботу та що має бути у списку залежностей.
Вправа з відлову помилок
Інтерв’юери люблять давати компоненти з помилками. Ось один із кількома проблемами.
function UserList({ users }) {
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.includes(search)
);
}, [users]);
useEffect(() => {
console.log("Search:", search);
}, []);
return (
<List
data={filteredUsers}
renderItem={({ item }) => (
<UserRow
user={item}
onPress={() => selectUser(item.id)}
/>
)}
/>
);
}
Відсутня залежність у useMemo
Мемо формується лише на основі користувачів:
}, [users]);
але фільтр також читає
search
тому список ніколи не оновлюється під час введення користувачем. У залежностях мають бути включені обидва елементи:
}, [users, search]);
Правило лінтингу негайно позначить це.
Ефект, заморожений на першому значенні
У цього ефекту є порожній масив:
useEffect(() => {
console.log(search);
}, []);
тому він виконується лише після монтажу та записує початковий search. Якщо мета — записувати кожну зміну, потрібно покладатися на цей ефект:
useEffect(() => {
console.log(search);
}, [search]);
Вбудований калебек, який обробляється з урахуванням обставин
Обробник рядка
onPress={() => selectUser(item.id)}
є новою функцією при кожному оновленні. Це саме по собі не є помилкою. Спочатку потрібно з’ясувати, чи
UserRow
це дороге та чи справді відбувається зайве оновлення. Лише на основі цих даних можна буде вирішити
useCallback
або переструктурувати компоненти, наприклад, передаючи id та стабільний обробник рядку. Демонстрація такої стриманості є частиною очікуваної відповіді.
Хороші, міцні та професійні відповіді
Послідовність легко запам’ятати. Хороший розробник знає useCallback. Компетентний розробник знає, коли його використовувати. Досвідчений розробник знає, коли його не використовувати, та може пояснити наслідки цього вибору. Та сама схема застосовується до кожного Hook. Метою ніколи не є
Maximum Hooks
а радше
Correct abstraction
+
Correct data flow
+
Correct performance trade-offs
Одна сукупність концепцій для вебу та нативних платформ
Вам не потрібні окремі знання про Hook для React та React Native. У веб-середовищі приклади зазвичай стосуються
DOM
browser events
Web APIs
WebSockets
browser storage
тоді як обговорення для мобільних пристроїв стосуються
AppState
Keyboard
native modules
device APIs
push notifications
network state
native event subscriptions
Проте основний набір інструментів у обох випадках — це React:
useState
useEffect
useRef
useMemo
useCallback
useContext
useReducer
Custom Hooks
closures
dependency arrays
Тож спочатку поясніть концепцію React, а потім покажіть, як її застосувати на конкретній платформі. Це збереже вашу авторитетність у ролях, пов’язаних із вебом, мобільними пристроями та гібридними рішеннями.
Швидкий посібник
useState
Елемент, який ініціює оновлення відображення після змін. Типові випадки використання:
form values
selected tabs
modal visibility
loading state
local UI state
useEffect
Синхронізація з зовнішніми системами. Типові випадки використання:
subscriptions
timers
WebSockets
AppState
native events
network operations
Уникайте його для значень, які можна отримати під час оновлення відображення.
useRef
Постійне змінне значення, яке не оновлює відображення. Типові випадки використання:
DOM/native refs
timers
previous values
imperative APIs
mutable non-UI data
useMemo
Значення, обчислене та збережене в пам’яті. Використовуйте його тоді,
calculation is expensive
or
referential identity matters
коли спочатку потрібно виміряти певні дані.
useCallback
Посилання на функцію, збережене в пам’яті. Воно корисне тоді,
function identity matters
React.memo is involved
another Hook depends on function identity
коли не потрібно автоматично створювати таке посилання.
useContext
Читає спільні дані контексту, наприклад:
theme
locale
authentication
configuration
useReducer
Моделює складні переходи. Корисно, коли у вас є
many related state variables
complex transitions
state machine-like behaviour
Custom Hooks
Багаторазово використовувана поведінка зі станом, наприклад:
useAuth()
useNetworkStatus()
useAppState()
usePagination()
useKeyboard()
useUserProfile()
Ментальна модель замість глосарію
Замість того, щоб запам’ятовувати пояснення до кожного Hook у вигляді одного слова,
useState = state
useEffect = side effect
useRef = reference
useMemo = memoisation
useCallback = callback
уявіть, як вони пов’язані з процесом відображення: стани інтерфейсу передаються через useState, що призводить до повторного відображення; похідні значення отримуються з useMemo; змінні дані, які не є частиною інтерфейсу, зберігаються в useRef; а useEffect з’єднує відображений інтерфейс з API, подіями та нативними системами, причому всі це обгортаються користувацькими Hooks.
React Render
│
┌──────────┼──────────┐
│ │ │
UI State Derived Mutable
│ Values Data
│ │ │
useState useMemo useRef
│
▼
Re-render
│
▼
UI
│
▼
External Systems
│
▼
useEffect
│
▼
APIs / Events / Native
│
▼
Custom Hooks
Під усім цим лежить кілька основних принципів:
Rendering
+
Closures
+
Referential Equality
+
Data Flow
+
Performance
Як тільки вони будуть добре засвоєні, окремі Hooks стануть зрозумілими.
Питання, які слід ставити під час кожного огляду коду
Hooks — це інструменти для керування різними видами поведінки в реактивних додатках, і важливою навичкою є розуміння того, чому ви використовуєте кожен з них. Ці питання корисні як під час огляду коду, так і під час співбесід:
- Чи справді цей стан потрібен, чи його можна отримати шляхом обчислень?
useState?Підсумок
Хуки стають простішими, як тільки ви перестанете сприймати їх як окремі API та будете бачити в них етапи одного потоку:
Rendering
↓
State
↓
Closures
↓
Effects
↓
References
↓
Memoisation
↓
Custom Hooks
↓
Performance
↓
Architecture
З огляду на цей підхід, перехід між React для вебу та React Native стає значно менш складним. Коли хтось запитує, чому ви використали useMemo у певному місці, пропустіть стандартне визначення та розкажіть історію: проблему, яку ви побачили, її причину, чому саме цей Hook підходить, які компроміси ви врахували та як оцінили результат.
Перед співбесідою переконайтеся, що можете природно говорити про:
- Основи: причини відображення контенту, що таке знімок відображення, у чому різниця між станом та звичайною змінною, закриття функцій, референційна рівність та її узгодження.
useContext та useReducer, спеціалізовані (useLayoutEffect, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue) та способи створення власних хуків.React.memo порівнює параметри, витрати на мемоізацію, профілювання відображень, довгі списки, складні обчислення та надмірно широкі підписки до сховища даних.Метою є не запам’ятовування відповідей на всі питання, а достатнє розуміння для того, щоб можна було обґрунтовано вирішити незнайому проблему під час співбесіди.