Пошук уроків, статей та іншого контенту
Розберемо асинхронність оновлень, пакетування, функціональні сетери та проблему застарілих значень State.
stateВиклик сетера, наприклад setCount, не змінює значення count миттєво. Він повідомляє React, що компонент потрібно повторно відрендерити з новим станом.
setCount(count + 1);
console.log(count);У цьому прикладі
console.logstateЦе можна уявити так:
React рендерить компонент зі значенням count = 0.
Обробник події отримує count = 0.
setCount(1) планує наступний рендер.
Поточний обробник усе ще бачить count = 0.
React виконує наступний рендер уже з count = 1.
Тому state у React поводиться як значення, зафіксоване для конкретного рендерингу.
React може об’єднати кілька оновлень стану в один повторний рендер. Це називається пакетуванням, або batching.
function handleClick() {
setFirstName("Анна");
setLastName("Коваль");
}React не обов’язково виконає два окремі рендеринги. Зазвичай оновлення будуть оброблені разом, а компонент перерендериться один раз із новими значеннями.
Пакетування зменшує кількість рендерингів і робить оновлення ефективнішими.
У сучасному React пакетування працює не лише безпосередньо в обробниках подій, а й для оновлень у setTimeout, Promise та інших асинхронних колбеках.
function handleAsyncUpdate() {
Promise.resolve().then(() => {
setStatus("Готово");
setMessage("Дані отримано");
});
}Обидва оновлення можуть бути оброблені разом. Водночас пакетування не означає, що кілька викликів сетера з одним і тим самим значенням автоматично збільшать його кілька разів.
Розглянемо такий код:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}На перший погляд здається, що count збільшиться на 3. Але якщо до обробника count дорівнював 0, усі три виклики фактично виглядають так:
setCount(1);
setCount(1);
setCount(1);Усі вони використовують одне й те саме значення count із поточного рендерингу. У результаті після пакетування стан стане 1, а не 3.
Це особливо важливо, коли нове значення залежить від попереднього:
збільшення лічильника;
додавання елемента до масиву;
перемикання прапорця;
оновлення числового значення на основі його попереднього значення.
Для оновлень, які залежать від попереднього стану, використовуйте функціональну форму сетера:
setCount(previousCount => previousCount + 1);Функція отримує найактуальніше значення стану, яке React передасть їй під час обробки черги оновлень.
Тому три виклики працюватимуть послідовно:
function handleClick() {
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
}Якщо початкове значення дорівнює 0, результатом буде 3.
React обробить оновлення приблизно так:
0 → 1 → 2 → 3Кожна наступна функція отримує результат попередньої.
Використовуйте його, якщо нове значення залежить від попереднього:
setScore(previousScore => previousScore + 10);
setItems(previousItems => [...previousItems, newItem]);
setIsOpen(previousIsOpen => !previousIsOpen);Якщо нове значення не залежить від попереднього, можна передати його безпосередньо:
setName("Олена");
setStatus("Завантажено");Асинхронна функція або колбек може зберегти значення state, яке було доступне в момент його створення. Таке значення називають застарілим, або stale value.
function handleDelayedIncrement() {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}Колбек усередині setTimeout запам’ятовує значення count із поточного рендерингу.
Наприклад:
count дорівнює 0.
Користувач запускає setTimeout.
До завершення таймера count змінюється на 5.
Таймер виконує setCount(count + 1).
Колбек усе ще може використовувати старе count = 0.
Значення встановиться в 1, хоча користувач очікував 6.
Для такого випадку потрібно використовувати функціональний сетер:
function handleDelayedIncrement() {
setTimeout(() => {
setCount(previousCount => previousCount + 1);
}, 1000);
}Тепер React використає актуальне значення безпосередньо перед оновленням.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("Очікування");
function incrementOnce() {
setCount(count + 1);
}
function incrementThreeTimesIncorrectly() {
// Усі виклики використовують одне значення count із поточного рендерингу
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
function incrementThreeTimesCorrectly() {
// Кожне оновлення отримує результат попереднього
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);
}
function incrementAfterDelay() {
setMessage("Значення буде збільшено через секунду");
setTimeout(() => {
// Функціональний сетер захищає від застарілого count
setCount(previousCount => previousCount + 1);
setMessage("Значення збільшено");
}, 1000);
}
function showCurrentValue() {
setCount(previousCount => {
console.log("Актуальне значення перед оновленням:", previousCount);
return previousCount + 1;
});
// Тут count ще належить поточному рендерингу
console.log("Значення в поточному обробнику:", count);
}
return (
<main>
<h1>Лічильник: {count}</h1>
<p>{message}</p>
<button onClick={incrementOnce}>
Збільшити на 1
</button>
<button onClick={incrementThreeTimesIncorrectly}>
Збільшити на 3 неправильно
</button>
<button onClick={incrementThreeTimesCorrectly}>
Збільшити на 3 правильно
</button>
<button onClick={incrementAfterDelay}>
Збільшити із затримкою
</button>
<button onClick={showCurrentValue}>
Оновити та вивести значення
</button>
</main>
);
}Кнопка «Збільшити на 3 неправильно» зазвичай збільшить лічильник лише на 1, тому що кожен виклик використовує старе значення count.
Кнопка «Збільшити на 3 правильно» використовує функціональні сетери, тому збільшить лічильник на 3.
У showCurrentValue також видно важливу особливість:
setCount(previousCount => previousCount + 1);
console.log(count);console.log не показує нове значення одразу. Нове значення стане доступним після наступного рендерингу компонента.
Іноді потрібно і змінити стан, і використати нове значення в поточному обробнику. Не варто чекати, що сетер поверне його синхронно. Обчисліть значення окремо:
function handleAdd() {
const nextCount = count + 1;
setCount(nextCount);
console.log("Наступне значення:", nextCount);
}Якщо ж оновлення може конкурувати з іншими оновленнями або виконується асинхронно, використовуйте функціональний сетер:
setCount(previousCount => previousCount + 1);Функціональний сетер є безпечним способом працювати з попереднім станом, але його функція має бути чистою: вона повинна обчислювати нове значення й не виконувати побічних дій.
setCount(count + 1);
console.log(count);console.log покаже значення поточного рендерингу, а не гарантовано нове значення.
setCount(count + 1);
setCount(count + 1);Якщо друге оновлення залежить від результату першого, використовуйте функціональну форму:
setCount(previousCount => previousCount + 1);
setCount(previousCount => previousCount + 1);setTimeout(() => {
setCount(count + 1);
}, 1000);Колбек може використати значення з попереднього рендерингу. Краще написати:
setTimeout(() => {
setCount(previousCount => previousCount + 1);
}, 1000);Кілька оновлень можуть бути пакетовані React в один рендеринг. Не слід покладатися на кількість викликів сетера як на кількість рендерингів.
Виклик сетера планує оновлення, але не змінює значення state у поточному обробнику.
Поточний рендеринг працює зі своїм зафіксованим значенням стану.
React може пакетувати кілька оновлень в один рендеринг.
Кілька викликів на кшталт setCount(count + 1) можуть використати одне й те саме застаріле значення.
Якщо новий стан залежить від попереднього, використовуйте функціональний сетер.
Функціональний сетер особливо важливий в асинхронних колбеках.
Щоб одразу працювати з майбутнім значенням у поточному обробнику, обчисліть його в локальній змінній.