useState
useState
— хук React, який дозволяє додати змінну стану до вашого компонента.
const [state, setState] = useState(initialState)
Опис
useState(initialState)
Викликачте useState
на верхньому рівні вашого компонента, щоб оголосити змінну стану.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Тейлор');
const [todos, setTodos] = useState(() => createTodos());
// ...
Зазвичай змінні стану називають як от [щось, setЩось]
, використовуючи деструктуризацію масиву.
Дивіться більше прикладів нижче.
Параметри
initialState
: Значення, яке ви хочете встановити як початкове для стану. Це може бути значення будь-якого типу, але якщо це функція — діють особливі правила. Цей аргумент ігнорується після першого рендеру.- Якщо ви передаєте функцію як
initialState
, вона спрацює як як функція-ініціалізатор. Вона має бути чистою, без аргументів, і повертати будь-яке значення. React викличе її під час ініціалізації компонента й збереже те, що вона поверне, як початковий стан. Дивіться приклад нижче.
- Якщо ви передаєте функцію як
Результат
useState
повертає масив, що містить рівно два значення:
- Поточний стан. Під час першого рендеру він дорівнює переданому
initialState
. - Функція
set
, яка дозволяє оновити стан на нове значення і викликає повторний рендер.
Застереження
useState
— це хук, тому його можна викликати лише на верхньому рівні вашого компонента або вашого власного хука. Не можна викликати його в циклах чи умовах. За потреби — перенесіть стан в новий компонент.- У режимі Strict Mode React буде викликати вашу функцію-ініціалізатор двічі, щоб допомогти виявити випадкові побічні ефекти. Це відбувається лише під час розробки і не впливає на продакшн. Якщо ваша функція-ініціалізатор чиста (якою вона і має бути), це не позначиться на поведінці компонента. Результат одного з викликів буде проігноровано.
Функції set
, наприклад setSomething(nextState)
Функція set
, яку повертає useState
, дозволяє оновити стан на інше значення і викликати повторний рендер. Ви можете передати нове значення напряму або функцію, яка обчислює його з попереднього стану:
const [name, setName] = useState('Едвард');
function handleClick() {
setName('Тейлор');
setAge(a => a + 1);
// ...
Параметри
nextState
: Значення, яке ви хочете встановити як новий стан. Це може бути значення будь-якого типу, але якщо це функція — діють особливі правила.- Якщо ви передасте функцію, React сприйме її як функцію-оновлювач. Вона має бути чистою, приймати поточний (ще не оновлений) стан як єдиний аргумент, і повертати нове значення стану. React додасть вашу функцію до черги оновлень і перерендерить компонент. Під час наступного рендеру React розрахує новий стан, послідовно застосовуючи всі функції з черги до попереднього стану. Дивіться приклад нижче.
Повернення
Функції set
не повертають значення
Застереження
-
Функція
set
оновлює змінну стану лише для наступного рендеру. Якщо звернутися до змінної стану одразу після викликуset
, ви все ще отримаєте попереднє значення, яке було актуальним до оновлення. -
Якщо нове значення, яке ви передаєте, ідентичне поточному
state
(згідно зObject.is
), React пропустить повторний рендер компонента та його нащадків. Це оптимізація. Хоча в деяких випадках React усе ж може викликати ваш компонент перед тим, як пропустити рендер нащадків, це не має вплинути на роботу вашого коду. -
React обробляє оновлення стану пакетно. Він оновлює інтерфейс після завершення всіх обробників подій, які викликали свої функції
set
. Це запобігає кільком перерендерам під час однієї події. У рідкісних випадках, коли вам потрібно примусово оновити інтерфейс раніше (наприклад, щоб звернутися до DOM), ви можете скористатисяflushSync
. -
Функція
set
має стабільну ідентичність, тож її часто не включають до залежностей у Effect. Але навіть якщо додати — це не спричинить повторного виклику Effect. Якщо лінтер дозволяє не вказувати залежність без помилок — так можна зробити. Дізнайтеся більше про видалення залежностей у Effect. -
Виклик функції
set
під час рендеру дозволено лише всередині компонента, що наразі рендериться. React відкине його поточний результат і негайно спробує перерендерити компонент з новим станом. Такий підхід потрібен рідко, але його можна використати, щоб зберігати інформацію з попередніх рендерів. Дивіться приклад нижче. -
У режимі Strict Mode React двічі викликає вашу функцію-оновлювач, щоб допомогти виявити випадкові побічні ефекти. Це відбувається лише під час розробки і не впливає на продакшн. Якщо ваша функція-оновлювач чиста (як і має бути), це не вплине на поведінку компонента. Результат одного з викликів буде проігноровано.
Використання
Додавання стану до компонента
Викликайте useState
на верхньому рівні вашого компонента, щоб оголосити одну або кілька змінних стану.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Тейлор');
// ...
Зазвичай змінні стану називають у форматі [щось, setЩось]
, використовуючи деструктуризацію масиву.
useState
повертає масив, що містить рівно два елементи:
- Поточний стан цієї змінної стану, який спочатку дорівнює початковому значенню, переданому вами.
- Функцію
set
, яка дає змогу змінювати цей стан у відповідь на взаємодію.
Щоб оновити інтерфейс, викличте функцію set
з новим станом:
function handleClick() {
setName('Робін');
}
React збереже новий стан, повторно відрендерить компонент із цими значеннями та оновить інтерфейс.
Приклад 1 із 4: Лічильник (число)
У цьому прикладі змінна стану count
зберігає число. Натискання кнопки збільшує його на одиницю.
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> Ви натиснули на мене {count} раз(ів) </button> ); }
Оновлення стану на основі попереднього
Припустимо, значення age
дорівнює 42
. Цей обробник викликає setAge(age + 1)
тричі:
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
Однак після одного кліка age
буде 43
, а не 45
! Це тому, що виклик функції set
не оновлює змінну age
у коді, що вже виконується. Тож кожен виклик setAge(age + 1)
стає setAge(43)
.
Щоб вирішити цю проблему, можна передати до setAge
функцію-оновлювач замість нового значення:
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
Тут a => a + 1
— це ваша функція-оновлювач. Вона приймає поточне значення стану та обчислює з нього наступне значення.
React ставить усі функції-оновлювачі у чергу. Під час наступного рендеру він викличе їх у тому ж порядку:
a => a + 1
отримає42
як поточне значення та поверне43
.a => a + 1
отримає43
як поточне значення та поверне44
.a => a + 1
отримає44
як поточне значення та поверне45
.
Інших оновлень у черзі немає, тож React зрештою збереже 45
як поточне значення стану.
Зазвичай аргумент функції-оновлювача називають першою літерою назви змінної стану — наприклад, a
для age
. Однак ви також можете використати назви типу prevAge
або будь-які інші, які вам зрозуміліші.
У режимі розробки React може викликати функції-оновлювачі двічі, щоб переконатися, що вони чисті.
Занурення
Ви могли чути пораду завжди писати код у стилі setAge(a => a + 1)
, якщо новий стан обчислюється на основі попереднього. Це не завдає шкоди, але й не завжди є необхідним.
У більшості випадків немає різниці між цими двома підходами. React завжди гарантує, що для свідомих дій користувача, як-от кліки, значення змінної age
буде оновлено до наступного кліку. Це означає, що обробник кліку не побачить “застаріле” значення age
на початку виконання.
Однак якщо ви виконуєте кілька оновлень у межах однієї події, функції-оновлювачі можуть бути корисними. Вони також зручні, коли доступ до змінної стану ускладнений (з цим можна зіткнутися під час оптимізації повторного рендеру).
Якщо для вас важлива послідовність навіть за ціною трохи багатослівнішого синтаксису, цілком логічно завжди використовувати функцію-оновлювач, коли новий стан залежить від попереднього. Якщо ж нове значення залежить від попереднього стану іншої змінної, можливо, варто об’єднати їх в один об’єкт і використати редюсер.
Приклад 1 із 2: Передавання функції-оновлювача
У цьому прикладі передається функція-оновлювач, тож кнопка “+3” працює як очікується.
import { useState } from 'react'; export default function Counter() { const [age, setAge] = useState(42); function increment() { setAge(a => a + 1); } return ( <> <h1>Ваш вік: {age}</h1> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <button onClick={() => { increment(); }}>+1</button> </> ); }
Оновлення стану, що містить об’єкти та масиви
У стан можна поміщати об’єкти та масиви. У React стан доступний лише для читання, тому його слід замінювати, а не змінювати наявні об’єкти. Наприклад, якщо у вас є об’єкт form
у стані, не змінюйте його ось так:
// 🚩 Не змінюйте об'єкт у стані безпосередньо:
form.firstName = 'Тейлор';
Замість цього замініть весь об’єкт, створивши новий:
// ✅ Замініть стан новим об'єктом
setForm({
...form,
firstName: 'Тейлор'
});
Про це докладніше у розділах оновлення об’єктів у стані та оновлення масивів у стані..
Приклад 1 із 4: Форма (об’єкт)
У цьому прикладі змінна стану form
зберігає об’єкт. Кожне поле вводу має обробник зміни, який викликає setForm
із новим станом усієї форми. Синтаксис розпакування { ...form }
гарантує, що об’єкт стану буде замінено, а не змінено напряму.
import { useState } from 'react'; export default function Form() { const [form, setForm] = useState({ firstName: 'Барбара', lastName: 'Гепворт', email: 'bhepworth@sculpture.com', }); return ( <> <label> Ім’я: <input value={form.firstName} onChange={e => { setForm({ ...form, firstName: e.target.value }); }} /> </label> <label> Прізвище: <input value={form.lastName} onChange={e => { setForm({ ...form, lastName: e.target.value }); }} /> </label> <label> Email: <input value={form.email} onChange={e => { setForm({ ...form, email: e.target.value }); }} /> </label> <p> {form.firstName}{' '} {form.lastName}{' '} ({form.email}) </p> </> ); }
Уникайте повторного створення початкового стану
React зберігає початковий стан один раз і ігнорує його під час наступних рендерів.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
Хоча результат createInitialTodos()
використовується лише під час першого рендеру, ви все одно викликаєте цю функцію на кожному рендері. Це може бути неефективно, якщо функція створює великі масиви або виконує ресурсоємні обчислення.
Щоб уникнути цього, у useState
передайте її як функцію-ініціалізатор:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
Зверніть увагу, що ви передаєте createInitialTodos
, тобто саму функцію, а не createInitialTodos()
, що є результатом її виклику. Якщо ви передаєте функцію в useState
, React викликає її лише під час ініціалізації.
У режимі розробки React може двічі викликати ваші ініціалізатори, щоб переконатися, що вони чисті.
Приклад 1 із 2: Передавання функції-ініціалізатора
У цьому прикладі передається функція-ініціалізатор, тому createInitialTodos
виконується лише під час ініціалізації. Вона не виконується під час повторних рендерів компонента, наприклад, коли ви щось вводите в поле.
import { useState } from 'react'; function createInitialTodos() { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: 'Елемент ' + (i + 1) }); } return initialTodos; } export default function TodoList() { const [todos, setTodos] = useState(createInitialTodos); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => { setText(''); setTodos([{ id: todos.length, text: text }, ...todos]); }}>Додати</button> <ul> {todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Скидання стану за допомогою key
Ви часто зустрічатимете атрибут key
при рендерінгу списків. Але він має ще одну цікаву властивість.
Ви можете скинути стан компонента, передавши йому інше значення key
. У цьому прикладі кнопка Reset змінює змінну стану version
, яку ми передаємо як key
компоненту Form
. Коли значення key
змінюється, React створює компонент Form
(і всіх його нащадків) заново, тож його стан скидається.
Докладніше читайте в розділі збереження і скидання стану.
import { useState } from 'react'; export default function App() { const [version, setVersion] = useState(0); function handleReset() { setVersion(version + 1); } return ( <> <button onClick={handleReset}>Скинути</button> <Form key={version} /> </> ); } function Form() { const [name, setName] = useState('Тейлор'); return ( <> <input value={name} onChange={e => setName(e.target.value)} /> <p>Привіт, {name}.</p> </> ); }
Збереження інформації з попередніх рендерів
Зазвичай ви оновлюєте стан у обробниках подій. Але іноді виникає потреба змінити стан у відповідь на сам рендер — наприклад, змінити стан, коли змінюється пропс.
У більшості випадків це не потрібно:
- Якщо значення можна обчислити на основі поточних пропсів або стану — взагалі не зберігайте його в стані. Якщо ви переймаєтеся через повторне обчислення, допоможе хук
useMemo
. - Якщо потрібно скинути стан усього піддерева компонентів — передайте новий
key
вашому компоненту. - Якщо можливо — оновлюйте увесь потрібний стан всередині обробників подій.
У рідкісних випадках, коли жодне з цих рішень не підходить, можна скористатися шаблоном, у якому функція set
викликається під час рендеру, аби оновити стан на основі значень, які вже були відрендерені.
Ось приклад. Компонент CountLabel
показує проп count
, який йому передано:
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
Скажімо, ви хочете показувати, чи значення лічильника зросло чи зменшилося з моменту останньої зміни. Сам по собі count
цього не показує — потрібно зберігати його попереднє значення. Додайте змінну стану prevCount
, щоб відстежувати його. Додайте також trend
, яка зберігатиме, зростає значення чи зменшується. Порівняйте prevCount
і count
, і якщо вони не рівні — оновіть обидва. Тепер ви можете показати не лише поточне значення, але й як воно змінилось.
import { useState } from 'react'; export default function CountLabel({ count }) { const [prevCount, setPrevCount] = useState(count); const [trend, setTrend] = useState(null); if (prevCount !== count) { setPrevCount(count); setTrend(count > prevCount ? 'зростає' : 'спадає'); } return ( <> <h1>{count}</h1> {trend && <p>Значення {trend}</p>} </> ); }
Зверніть увагу: якщо ви викликаєте set
під час рендеру, це має бути всередині умови на кшталт prevCount !== count
, і в тій же умові має бути виклик setPrevCount(count)
. Інакше компонент зациклиться і зламається. Крім того, так можна оновлювати стан лише поточного компонента. Виклик set
в іншому компоненті під час рендеру — помилка. Нарешті, виклик set
усе одно має оновлювати стан без мутацій, також це не дозвіл порушувати інші правила чистих функцій.
Цей патерн досить складний для розуміння і зазвичай краще його уникати. Але він кращий, ніж оновлення стану в ефекті. Коли ви викликаєте set
під час рендеру, React виконає повторний рендер компонента одразу після return
, до того, як почне рендерити дочірні. Тобто, дочірні компоненти не рендеряться двічі. Решта вашої функції-компонента все одно виконається (але її результат буде відкинуто). Якщо умова виклику set
йде після всіх хуків — можна зробити ранній return;
, аби почати рендер раніше.
Поширені проблеми
Я оновив стан, але в консолі виводиться старе значення
Виклик функції set
не змінює стан у виконуваному коді:
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Запит на ререндер із 1
console.log(count); // Все ще 0!
setTimeout(() => {
console.log(count); // Також 0!
}, 5000);
}
Це тому, що стан поводиться як знімок (snapshot). Оновлення стану — це запит на новий рендер із новим значенням стану, але воно не змінює змінну count
у вже виконуваному обробнику подій.
Якщо вам потрібно використати наступний стан — збережіть його в змінну перед переданням у set
:
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
Я оновив стан, але інтерфейс не оновлюється
React ігноруватиме ваше оновлення, якщо новий стан ідентичний попередньому, згідно з порівнянням за Object.is
. Це зазвичай трапляється, коли ви напряму змінюєте об’єкт або масив у стані:
obj.x = 10; // 🚩 Помилка: мутація існуючого об’єкта
setObj(obj); // 🚩 Не спрацює
Ви змінили існуючий об’єкт obj
і знову передали його у setObj
, тому React проігнорував оновлення. Щоб це виправити, потрібно завжди замінювати об’єкти й масиви у стані замість того, щоб мутувати їх:
// ✅ Правильно: створення нового об’єкта
setObj({
...obj,
x: 10
});
Я отримую помилку: “Too many re-renders”
Ви можете побачити помилку: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Зазвичай це означає, що ви безумовно викликаєте оновлення стану під час рендеру, тому компонент входить у цикл: рендер, оновлення стану (яке спричиняє рендер), рендер, оновлення стану і так далі. Дуже часто це трапляється через помилку в написанні обробника подій:
// 🚩 Помилка: обробник викликається під час рендеру
return <button onClick={handleClick()}>Натисни мене</button>
// ✅ Правильно: передається посилання на обробник
return <button onClick={handleClick}>Натисни мене</button>
// ✅ Правильно: передається стрілочна функція
return <button onClick={(e) => handleClick(e)}>Натисни мене</button>
Якщо не можете знайти причину цієї помилки, натисніть стрілку поруч із повідомленням у консолі та перегляньте стек викликів JavaScript, щоб знайти конкретний виклик функції set
, що спричиняє помилку.
Моя функція-ініціалізатор або функція-оновлювач виконується двічі
У режимі Strict Mode React викликає деякі з ваших функцій двічі замість одного разу:
function TodoList() {
// Ця функція-компонент виконуватиметься двічі під час кожного рендеру.
const [todos, setTodos] = useState(() => {
// Ця функція-ініціалізатор буде викликана двічі під час ініціалізації.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// Ця функція-оновлювач буде викликана двічі при кожному кліку.
return [...prevTodos, createTodo()];
});
}
// ...
Це очікувана поведінка і вона не повинна порушити логіку вашого коду.
Це лише для розробки і допомагає зберігати компоненти чистими. React використовує результат одного з викликів і ігнорує результат іншого. Якщо ваш компонент, ініціалізатор і функція-оновлювач є чистими, ця поведінка не повинна вплинути на логіку. Якщо ж вони мають побічні ефекти — це допоможе виявити помилки.
Наприклад, ось ця нечиста функція-оновлювач мутує масив у стані:
setTodos(prevTodos => {
// 🚩 Помилка: мутація стану
prevTodos.push(createTodo());
});
Оскільки React викликає вашу функцію-оновлювач двічі, ви побачите, що todo додано двічі — це сигналізує про помилку. У цьому випадку можна виправити це замінюючи масив новим, а не мутуючи його:
setTodos(prevTodos => {
// ✅ Правильно: створення нового стану
return [...prevTodos, createTodo()];
});
Тепер, коли ця функція-оновлювач чиста, її повторний виклик не змінює поведінки. Саме тому подвійний виклик допомагає виявляти помилки. Лише функції компонента, ініціалізатора та оновлення повинні бути чистими. Обробники подій не повинні бути чистими, тож React ніколи не викликатиме їх двічі.
Дізнайтесь більше з розділу про чистоту компонентів.
Я намагаюся зберегти у стан функцію, але вона викликається замість цього
Не можна зберігати функцію у стан ось так:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
Оскільки ви передаєте функцію, React припускає, що someFunction
— це функція-ініціалізатор, а someOtherFunction
— функція-оновлювач, тому намагається викликати їх і зберегти результат. Щоб насправді зберегти функцію, потрібно додати () =>
перед ними в обох випадках. Тоді React збереже передані вами функції.
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}