Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює useState у React?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useState** - це хук React для додавання локального стану до функціональних компонентів. ```jsx const [count, setCount] = useState(0); setCount(5); // задати конкретне значення setCount(c => c + 1); // функціональне оновлення: читає останнє значення з черги ``` **Головне:** сеттер ставить оновлення в чергу, а не змінює стан одразу. Використовуй функціональну форму, коли нове значення залежить від поточного.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useState** - це хук React, який дозволяє функціональному компоненту зберігати локальний стан. Повертає поточне значення і функцію-сеттер, яка ставить перерендер у чергу. ## Теорія ### TL;DR - Уяви дисплей торгового автомату: показує поточне число, ти натискаєш кнопку (сеттер) і нове значення ставиться в чергу для наступного циклу (рендер). - Стан зберігається між рендерами. Звичайна змінна всередині компонента скидається при кожному виклику функції. - Сеттер не оновлює стан одразу. Він ставить оновлення в чергу, і React об'єднує кілька викликів в один рендер. - `useState` - для локальних даних компонента. Для спільного стану між компонентами - `useContext`. Для складної логіки оновлень - `useReducer`. - Коли нове значення залежить від попереднього, завжди використовуй функціональну форму: `setState(prev => next)`. ### Швидкий приклад ```jsx import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // [поточне значення, сеттер] return ( <div> <p>Лічильник: {count}</p> <button onClick={() => setCount(c => c + 1)}> + {/* функціональне оновлення читає останнє значення з черги */} </button> </div> ); } ``` `useState(0)` задає початкове значення `0`. Клік по кнопці викликає сеттер з функцією, яка отримує останній стан і повертає наступне значення. React ставить рендер у чергу, і компонент покаже `1`. ### Головна відмінність На відміну від звичайної змінної, стан зберігається між рендерами. Але сеттер не змінює значення під час поточного рендеру. Він ставить оновлення в чергу, і React застосовує всі накопичені оновлення разом перед наступним відображенням. Саме тому, якщо прочитати `count` одразу після `setCount(count + 1)`, отримаєш старе значення. Нове значення з'явиться тільки в наступному рендері. ### Коли використовувати - Локальне перемикання стану (відкрити/закрити модалку, вибір вкладки) - один `useState` на булеве значення. - Контрольовані поля форми - `useState` на поле, або один `useState({})` для всієї форми. - Незалежні значення - окремі виклики `useState`, а не один великий об'єкт. - Логіка оновлення з кількома підзначеннями або умовами - `useReducer` буде зрозумілішим. - Стан, потрібний кільком компонентам - підніми його вище або використовуй `useContext` з `useState` на вищому рівні. - Нове значення залежить від поточного стану - завжди функціональна форма, щоб уникнути застарілих читань. ### Як useState зберігає стан React зберігає дані хуків кожного компонента у **fiber node** - внутрішньому об'єкті в дереві компонентів. Кожен виклик `useState` отримує один слот у зв'язному списку, прикріпленому до цього fiber. Перший `useState` - слот 0, другий - слот 1, і так далі. Саме тому виклик хуків всередині умов ламає React. Якщо `useState` стоїть всередині `if`, порядок слотів змінюється між рендерами і React читає неправильний стан з неправильного слота. Правило "викликай хуки тільки на верхньому рівні" існує саме з цієї причини. Коли ти викликаєш сеттер (всередині React він називається `dispatchAction`), React додає об'єкт оновлення до черги цього слота. При наступному рендері React проходить усі хуки по порядку, обробляє чергу оновлень кожного і обчислює новий стан. У React 18 оновлення всередині подій, `setTimeout` і `Promise.then` автоматично об'єднуються в один рендер. У React 17 так об'єднувалися тільки оновлення всередині обробників подій. ### Лінива ініціалізація Передай функцію в `useState`, коли обчислення початкового значення є дорогою операцією: ```jsx // compute() виконується на кожному рендері, результат ігнорується після mount const [data, setData] = useState(compute()); // compute() виконується лише один раз, при першому рендері const [data, setData] = useState(() => compute()); ``` Цей підхід називається lazy initialization (лінива ініціалізація). React викликає функцію рівно один раз і використовує повернуте значення як початковий стан. Підходить для читання з `localStorage`, парсингу URL або обробки великого набору початкових даних. ### Типові помилки **Пряма мутація стану:** ```jsx const [arr, setArr] = useState([1, 2]); arr.push(3); // React бачить ту саму посилання, рендер не відбудеться setArr(arr); // Та сама посилання, React пропускає оновлення // Правильно: setArr([...arr, 3]); // або функціональна форма: setArr(a => [...a, 3]); ``` **Читання стану одразу після виклику сеттера:** ```jsx const handleClick = () => { setCount(count + 1); console.log(count); // Виведе старе значення. Оновлення ще в черзі. }; ``` **Застарілий closure при кількох викликах сеттера:** ```jsx // Обидва виклики читають однаковий count з поточного рендеру const handleClick = () => { setCount(count + 1); // count=0, ставить: встановити в 1 setCount(count + 1); // count=0, те саме замикання, ставить: встановити в 1 знову // Результат: 1, а не 2 }; // Правильно: функціональні оновлення правильно ланцюжаться const handleClick = () => { setCount(c => c + 1); // c=0, результат: 1 setCount(c => c + 1); // c=1, результат: 2 }; ``` **Неповне копіювання вкладених об'єктів:** ```jsx const [user, setUser] = useState({ name: 'Alex', settings: { theme: 'light' } }); // Неправильно: settings досі вказує на оригінальний об'єкт user.settings.theme = 'dark'; setUser(user); // та сама посилання, React пропускає рендер // Правильно: копіюй кожен рівень, який змінюєш setUser(u => ({ ...u, settings: { ...u.settings, theme: 'dark' } })); // Ще краще: розбий на окремі стани, якщо значення незалежні const [theme, setTheme] = useState('light'); ``` **Виклик сеттера під час рендеру:** ```jsx function Bad() { const [x, setX] = useState(0); setX(1); // Нескінченний цикл: сеттер викликає рендер, рендер викликає сеттер // Правильно: перемісти в useEffect або обробник події } ``` Один патерн, який навіть досвідчені розробники часто пропускають: виклик `setState` всередині `forEach` по поточному стану. Кожна ітерація захоплює той самий застарілий масив, і виживає тільки останнє оновлення. Рішення - функціональна форма або побудова повного наступного стану до єдиного виклику сеттера. ### Де зустрічається в реальному коді - Патерн React TodoMVC - `useState([])` для списку задач, окремий `useState` для активного фільтра. - Форми у Next.js - `useState({})` для контрольованих полів, валідація перед відправкою. - Перемикач теми - `useState('light')` з синхронізацією в `localStorage` при кожній зміні. - React DevTools - відстежує стан компонентів через кілька `useState` на компонент. - Клієнтська валідація у Remix - `useState` зберігає повідомлення про помилки до відправки форми. ### Питання на співбесіді **Q:** Що станеться, якщо викликати сеттер під час рендеру? **A:** React кине помилку "Too many re-renders." Сеттер викликає рендер, рендер знову викликає сеттер - нескінченний цикл. Перемісти оновлення стану в обробник події або `useEffect`. **Q:** Що таке lazy initialization і коли її використовувати? **A:** Передача функції в `useState` замість значення. React викликає її один раз при монтуванні. Підходить, коли обчислення початкового значення є дорогим: читання `localStorage`, парсинг великого набору даних. **Q:** Яка різниця між `setState(value)` і `setState(fn)`? **A:** `setState(value)` захоплює значення з поточного замикання (closure). Якщо кілька сеттерів об'єднуються в batch, всі вони читають той самий застарілий стан. `setState(fn)` отримує найновіше значення з черги оновлень як аргумент, тому ланцюжкові оновлення працюють коректно. **Q:** Як auto-batching у React 18 змінює поведінку useState? **A:** До React 18 об'єднувалися тільки оновлення всередині синтетичних обробників подій. React 18 також об'єднує оновлення в `setTimeout`, `Promise.then` і нативних обробниках, зменшуючи кількість зайвих рендерів. Код, який розраховував на окремий рендер для кожного асинхронного виклику сеттера, може поводитися інакше. **Q:** (Senior) Чому хуки завжди мають викликатися в однаковому порядку? **A:** React відстежує кожен `useState` за позицією у зв'язному списку fiber. Якщо порядок змінюється між рендерами (хук всередині `if`), індекс збивається і React читає неправильний стан з неправильного слота. Лінтер для хуків виловлює це на рівні інструментів. **Q:** (Senior) Якщо викликати `setCount` з тим самим значенням, що й поточний стан, чи відбудеться рендер? **A:** Ні. React порівнює нове і поточне значення через `Object.is`. Якщо вони рівні, React пропускає рендер повністю. Це вбудована оптимізація, яку не потрібно додавати вручну. ## Приклади ### Базовий: показати і приховати модалку ```jsx import { useState } from 'react'; function Modal() { const [isOpen, setIsOpen] = useState(false); return ( <div> <button onClick={() => setIsOpen(o => !o)}> {isOpen ? 'Закрити' : 'Відкрити'} модалку </button> {isOpen && <div className="modal">Вміст модалки</div>} </div> ); } ``` `isOpen` починається як `false`. Кнопка перемикає його через функціональне оновлення. React перерендерює і або показує, або ховає `div`. Зовнішній стейт-менеджмент тут зайвий. ### Середній рівень: контрольована форма входу ```jsx import { useState } from 'react'; function LoginForm() { const [form, setForm] = useState({ email: '', password: '' }); const [error, setError] = useState(''); const handleChange = (e) => { const { name, value } = e.target; setForm(f => ({ ...f, [name]: value })); // оновлюємо одне поле, решта без змін }; const handleSubmit = (e) => { e.preventDefault(); if (!form.email.includes('@')) { setError('Введи коректний email'); return; } setError(''); // відправити form.email і form.password на API }; return ( <form onSubmit={handleSubmit}> <input name="email" value={form.email} onChange={handleChange} /> <input name="password" type="password" value={form.password} onChange={handleChange} /> {error && <p style={{ color: 'red' }}>{error}</p>} <button type="submit">Увійти</button> </form> ); } ``` Два окремих `useState` відповідають за різні речі: один для значень полів, інший для повідомлення про помилку. Функціональне оновлення в `handleChange` розпаковує попередню форму і замінює тільки змінене поле, що уникає пастки з неповним копіюванням вкладеного стану. ### Просунутий рівень: застарілий closure і batching у React 18 ```jsx import { useState } from 'react'; function BadCounter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); // count=0, ставить: встановити в 1 setCount(count + 1); // count=0, те саме замикання, ставить: встановити в 1 знову // Після кліку: count = 1, а не 2 }; return <button onClick={handleClick}>Погано: {count}</button>; } function GoodCounter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(c => c + 1); // c=0, результат: 1 setCount(c => c + 1); // c=1, результат: 2 // Після кліку: count = 2 }; return <button onClick={handleClick}>Добре: {count}</button>; } ``` `BadCounter` використовує `count` напряму в обох викликах. React 18 об'єднує їх в один рендер, але обидва замикання захоплюють однаковий `count` (нуль). Черга оновлень містить дві однакові інструкції: "встановити в 1." Результат: `1`. `GoodCounter` використовує функціональні оновлення. React передає в кожен callback найновіше значення з черги, тому вони ланцюжаться: `0 -> 1 -> 2`. Це правильний патерн для будь-якого обробника, який викликає той самий сеттер більше одного разу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.