useState: черги оновлень та асинхронна природа.Поява хуків (Hooks) у React 16.8 стала революцією, що раз і назавжди змінила підхід до написання компонентів. Хук useState здається неймовірно простим на перший погляд, проте його невірно сприймають як просто “змінну, що змушує екран оновлюватись”. Для інженерів 4-го курсу важливо розуміти, що стан у React — це не миттєвий показник, а план (запит) на рендер у майбутньому. Незнання механік черг оновлень (Update Queues) та пакетування (Batching) є найчастішою причиною багів у складних асинхронних інтерфейсах (таких як гонки даних — Race Conditions). Ця лекція присвячена правильному інженерному управлінню локальним станом компонента.
До появи хуків перевикористання логіки роботи зі станом досягалося через патерни HOC (Higher-Order Components) та Render Props, що призводило до “Пекла обгорток” (Wrapper Hell) — нескінченної вкладеності компонентів у дереві React Developer Tools.
Хуки вирішили цю проблему, дозволивши використовувати власні (custom) функції use.... Але як звичайна JavaScript-функція “пам’ятає” дані між рендерами, якщо всі її локальні змінні знищуються після виконання return?
Секрет у React Fiber:
На стадії рендерингу (у пам’яті), кожному компоненту відповідає об’єкт Fiber Node. Всередині Fiber Node є поле memoizedState. Хуки у React реалізовані як структура даних однобічно зв’язного списку (Singly Linked List).
Коли ви викликаєте підряд 3 хуки:
const [name, setName] = useState("Alex"); // Хук 1
const [age, setAge] = useState(20); // Хук 2
useEffect(() => {}, []); // Хук 3
React будує під капотом ланцюжок: Hook1 -> Hook2 -> Hook3. Наступного разу, коли компонент відрендериться знову, React пройдеться по цьому тому ж самому ланцюжку в тому ж самому порядку, відновивши збережені значення для name та age.
Оскільки React покладається виключно на порядок виклику хуків під час обходу зв’язного списку (Linked List), виклик хука всередині блоку if або for зруйнує всю архітектуру.
Фатальна помилка (Антипатерн):
const UserProfile = ({ isLoggedIn }) => {
if (isLoggedIn) {
const [lastLogin, setLastLogin] = useState(Date.now()); // ПОМИЛКА!
}
const [theme, setTheme] = useState("dark");
return <div>{theme}</div>;
};
Якщо під час другого рендеру isLoggedIn стане false, перший хук не викличеться. React спробує дістати другий елемент зв’язного списку (де зберігалася тема 'dark') і помилково підставить його у setLastLogin, повністю зламавши логіку додатка.
Два золоті правила хуків:
return).Контроль: Забезпечується плагіном лінтера eslint-plugin-react-hooks, який аналізує AST дерева вашого коду на етапі написання.
useState: черги оновлень та асинхронна природаСинтаксис хука використовує деструктуризацію масиву: const [state, setState] = useState(initialValue).
Найбільша омана новачків:
Виклик setState не змінює стан одразу (синхронно) в поточному коді. Функція setState лише додає об’єкт-запит (Update Object) до черги оновлень (Update Queue) компонента і повідомляє React: “Заплануй рендер цього компонента наново”.
const Counter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // setCount(0 + 1) -> Заплановано рахунок 1
setCount(count + 1); // setCount(0 + 1) -> Заплановано рахунок 1
setCount(count + 1); // setCount(0 + 1) -> Заплановано рахунок 1
// Тут count ВСЕ ЩЕ дорівнює 0!
console.log(count); // Виведе 0, а не 3
};
// Після завершення цієї функції, React робить ОДИН рендер і count стає = 1.
};
Змінна count у функціональному компоненті є константою (const). Вона зафіксована для поточного рендеру (closure / замикання). Оновлення стану викликає нове виконання всієї функції-компонента з вже новою зафіксованою const count = 1.
Що робити, якщо нам дійсно потрібно оновити стан тричі підряд, залежачи від попереднього результату? Або якщо функція setCount викликається всередині setTimeout або setInterval, які дивляться на застаріле замикання (stale closure)?
Для цього використовується патерн передачі функції в setState замість жорсткого значення.
const BetterCounter = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
// React покладе ці три функції в чергу (Queue)
setCount((prevCount) => prevCount + 1); // React виконає: 0 + 1 (результат 1)
setCount((prevCount) => prevCount + 1); // React виконає: 1 + 1 (результат 2)
setCount((prevCount) => prevCount + 1); // React виконає: 2 + 1 (результат 3)
};
// Компонент перерендериться один раз і count дорівнюватиме 3.
};
Інженерне правило: Якщо наступний стан обчислюється на основі поточного стану — ЗАВЖДИ використовуйте функціональне оновлення.
В React 17 та нижче існувала проблема продуктивності під час асинхронних операцій (fetch, setTimeout).
Якщо ви викликали кілька оновлень стану всередині .then() після fetch-запиту, React робив окремий рендер для кожного setState.
React 18 впровадив Automatic Batching (Автоматичне пакетування).
Тепер React групує УСІ виклики setState у межах одного макро/мікрозавдання (Event Loop) і викликає рендер рівно 1 раз.
const fetchUser = async () => {
setIsLoading(true);
const data = await api.getUser(); // пауза
// React 18: Обидва ці оновлення згрупуються (Batching)
setUser(data.user);
setIsLoading(false);
// React зробить ОДИН рендер компонента замість двох.
};
Якщо винятково потрібно форсувати негайний рендеринг (щоб прочитати розміри ДОМ елемента після зміни) використовують імперативний метод flushSync з react-dom, проте це є архітектурним виключенням і використовується рідко.
Аргумент, переданий у useState, використовується виключно під час першого рендеру (ініціалізації) компонента. У всіх наступних рендерах це значення просто ігнорується рушієм.
Якщо ініціалізація вимагає важкої операції (наприклад, парсинг гігантського масиву з localStorage або обчислення фібоначчі), використання звичайного підходу “вб’є” процесор:
// ПОГАНО: getHeavyData() виконується при КОЖНОМУ кліці і зміні стану, хоча React відкине результат після першого разу!
const [data, setData] = useState(getHeavyData());
Щоб вирішити цю проблему без використання useEffect, застосовують Lazy Initialization (Ледачу ініціалізацію) — передачу функції без її безпосереднього виклику:
// ДОБРЕ: React викличе getHeavyData лише один раз при першому монтуванні компонента.
const [data, setData] = useState(() => getHeavyData());
Найбільша архітектурна помилка, яку допускають розробники — це збереження значення в useState, яке можна легко обчислити математично під час рендеру. Це призводить до несинхронізованих багів (Out of Sync).
Антипатерн (Дублювання):
const Cart = () => {
const [items, setItems] = useState([{ price: 10 }, { price: 20 }]);
// ПОМИЛКА: Це похідний стан. Його НЕ ТРЕБА зберігати в пам'яті через useState.
const [total, setTotal] = useState(30);
const addItem = (item) => {
setItems([...items, item]);
// Легко забути оновити total, або зробити помилку в обчисленнях
setTotal(total + item.price);
};
};
Інженерно правильне рішення (Derived State обчислюється “на льоту”):
const Cart = () => {
const [items, setItems] = useState([{ price: 10 }, { price: 20 }]);
// ТА-ДАМ! Ми просто обчислюємо це при кожному рендері.
// Джерело істини (Single Source of Truth) залишається ОДНЕ — це масив items!
const total = items.reduce((sum, item) => sum + item.price, 0);
// Для ВЕЛИКИХ масивів можна обгорнути обчислення в хук оптимізації useMemo.
// const total = useMemo(() => items.reduce(...), [items]);
};
Створення похідного стану в useState чи useEffect — це марне споживання оперативної пам’яті та зайві цикли рендерингу. Розраховуйте все, що можете розрахувати, безпосередньо в тілі компонента.
useState в React працює не як проста змінна, а як диспетчер черг оновлень, який вписує запити на новий стан у структуру даних Singly Linked List в об’єкті Fiber.setState(prev => prev + 1)).useState всередину простої умови if або викликав після слова return?Event Loop та процесів React 18 “Automatic Batching”, чому три підряд виклики setCount(0 + 1) створять лише один рендер із кінцевим результатом 1, а не 3?useState(() => init())?useState вважається критичною архітектурною помилкою?setCount(prev => prev + 1)) кілька разів підряд?