Правила хуків
Хуки — це нововведення React 16.8. Вони дозволяють вам використовувати стан та інші можливості React без написання класів.
Хуки — звичайні JavaScript-функції, але використовуючи їх, ви маєте дотримуватися двох правил. Ми зробили плагін для лінтера, щоб автоматично дотримуватися цих правил:
Використовуйте хуки тільки на найвищому рівні
Не використовуйте хуки усередині циклів, умовних операторів або вкладених функцій. Натомість, завжди використовуйте хуки на найвищому рівні React-функцій. Дотримуючись цього правила, ви будете певні, що хуки викликаються в однаковій послідовності кожного разу, коли рендериться компонент. Це дозволяє React коректно зберігати стан хуків між численними викликами useState
та useEffect
. (Якщо вам цікаво, то ми пояснимо це більш детально нижче.)
Викликайте хуки лише з React-функцій
Не викликайте хуки зі звичайних JavaScript-функцій. Натомість, ви можете:
- ✅ Викликати хуки з функціонального компоненту React.
- ✅ Викликати хуки з користувацьких хуків (ми навчимося це робити на наступній сторінці).
Дотримуючись цього правила, ви можете бути певні, що вся логіка компонента зі станом чітко проглядається в його джерельному коді.
Плагін для ESLint
Ми випустили плагін для ESLint eslint-plugin-react-hooks
, який примушує дотримуватися цих двох правил. Ви можете додати цей плагін до свого проекту, якщо ви хочете його спробувати:
Цей плагін використовується за замовчуванням у Create React App.
npm install eslint-plugin-react-hooks --save-dev
// Ваша конфігурація ESLint
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Перевіряє правила хуків
"react-hooks/exhaustive-deps": "warn" // Перевіряє ефект залежностей
}
}
Ви можете пропустити залишок сторінки та перейти до наступної, яка пояснює як писати користувацькі хуки. На цій сторінці ми продовжимо і пояснимо необхідність цих правил.
Пояснення
Як ми дізналися раніше, в одному компоненті можна використовувати декілька хуків стану або ефектів:
function Form() {
// 1. Використовуємо змінну стану name
const [name, setName] = useState('Ліна');
// 2. Використовуємо ефект для збереження стану форми
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Використовуємо змінну стану surname
const [surname, setSurname] = useState('Костенко');
// 4. Використовуємо ефект, щоб оновити заголовок сторінки
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
Отже, як React дізнається, який стан відповідає певному виклику useState
? Відповідь наступна: React покладається на послідовність викликів хуків. Наш приклад працює тому, що послідовність викликів хуків є сталою для кожного рендеру:
// ------------
// Перший рендер
// ------------
useState('Ліна') // 1. Ініціюємо змінну name зі значенням 'Ліна'
useEffect(persistForm) // 2. Додаємо ефект, щоб зберегти данні форми
useState('Костенко') // 3. Ініціюємо змінну surname зі значенням 'Костенко'
useEffect(updateTitle) // 4. Додаємо ефект, щоб оновити заголовок сторінки
// -------------
// Другий рендер
// -------------
useState('Ліна') // 1. Зчитуємо змінну стану name (аргумент ігнорується)
useEffect(persistForm) // 2. Змінюємо ефект, щоб зберегти данні форми
useState('Костенко') // 3. Зчитуємо змінну стану surname (аргумент ігнорується)
useEffect(updateTitle) // 4. Змінюємо ефект, щоб оновити заголовок сторінки
// ...
Доки послідовність викликів хуків залишається сталою між рендерами, React може співвідносити локальний стан з кожним із них. Але що трапиться, якщо ми розмістимо виклик хуку (наприклад, ефект persistForm
) всередину умовного оператору?
// 🔴 Ми порушуємо перше правило, розміщуючи хук всередині умовного оператору
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
Умова name !== ''
дорівнює true
при першому рендері, тому цей хук буде виконано. Хай там що, та в наступному рендері користувач може очистити форму і таким чином змінити цю умову на false
. Тепер, оскільки ми пропускаємо цей хук під час рендеру, послідовність викликів хуків стає іншою:
useState('Ліна') // 1. Зчитуємо змінну стану name (аргумент ігнорується)
// useEffect(persistForm) // 🔴 Цей хук пропущено!
useState('Костенко') // 🔴 2 (але був 3). Помилка при зчитуванні змінної стану surname
useEffect(updateTitle) // 🔴 3 (but was 4). Помилка при зміні ефекту
React не знатиме, що повернути для другого виклику хуку useState
. React очікував, що другий виклик хуку в цьому компоненті відповідає ефекту persistForm
так само як і під час попереднього рендеру, але це більше не так. З цього моменту кожен наступний виклик хуку після того, що ми пропустили, також зміститься на одиницю, що призведе до помилок.
Ось чому хуки мають викликатися на найвищому рівні наших компонентів. Якщо ми хочемо викликати ефект за певної умови, то ми можемо розмістити цю умову всередині нашого хуку:
useEffect(function persistForm() {
// 👍 Більше ми не порушимо перше правило
if (name !== '') {
localStorage.setItem('formData', name);
}
});
Зауважте, що вам не потрібно буде піклуватися про цю проблему, якщо ви додасте до вашого проекту запропоноване правило для лінтера. Але тепер ви знаєте, чому хуки працюють таким чином, та яким проблемам можна запобігти, використовуючи це правило.
Наступні кроки
Нарешті ми можемо почати вчитися тому, як писати користувацькі хуки! Хуки користувача дозволять вам комбінувати хуки, впроваджені React, з вашими власними абстракціями, та повторно використовувати одну й ту ж логіку стану в різних компонентах.