Пошук уроків, статей та іншого контенту
Вивчите правила виклику Hooks і зрозумієте, чому їх не можна використовувати в умовах, циклах та звичайних функціях.
Hooks — це спеціальні функції React, які дають змогу використовувати стан, ефекти та інші можливості React у функціональних компонентах.
До вбудованих Hooks належать:
useState — для збереження стану;
useEffect — для виконання побічних ефектів;
useContext — для отримання значення з контексту;
useReducer — для складнішого керування станом.
Назва кожного Hook починається з префікса use. Наприклад:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Кількість: {count}
</button>
);
}useState викликається всередині функціонального компонента Counter.
React має два основні правила:
Викликайте Hooks лише на верхньому рівні компонента або власного Hook.
Викликайте Hooks лише з функціональних компонентів React або власних Hooks.
Ці правила потрібні, щоб React міг правильно зберігати стан і пов’язувати його з конкретним викликом Hook.
Виклик Hook не можна розміщувати всередині if, тернарного оператора або іншої умовної конструкції.
Неправильно:
function UserProfile({ isLoggedIn }) {
if (isLoggedIn) {
const [name, setName] = useState("Анна");
}
return <div>Профіль користувача</div>;
}Якщо isLoggedIn зміниться, useState може бути викликаний в одному рендері, але не викликаний в іншому. Через це порядок Hooks порушиться.
Правильно — викликати Hook завжди, а умову використовувати для значення або відображення:
import { useState } from "react";
function UserProfile({ isLoggedIn }) {
const [name, setName] = useState("Анна");
if (!isLoggedIn) {
return <p>Увійдіть, щоб переглянути профіль.</p>;
}
return (
<section>
<h2>Профіль</h2>
<p>Користувач: {name}</p>
<button onClick={() => setName("Олена")}>
Змінити ім’я
</button>
</section>
);
}
export default UserProfile;У цьому прикладі useState викликається під час кожного рендера, незалежно від значення isLoggedIn.
Неправильно:
function Products() {
const products = ["Книга", "Ноутбук", "Телефон"];
for (const product of products) {
const [isSelected, setIsSelected] = useState(false);
}
return <p>Список товарів</p>;
}Кількість і порядок викликів Hook можуть змінитися, якщо зміниться масив products.
Якщо кожен елемент списку має власний стан, створіть окремий компонент:
import { useState } from "react";
function ProductItem({ name }) {
const [isSelected, setIsSelected] = useState(false);
return (
<li>
<button onClick={() => setIsSelected(!isSelected)}>
{name}: {isSelected ? "обрано" : "не обрано"}
</button>
</li>
);
}
function Products() {
const products = ["Книга", "Ноутбук", "Телефон"];
return (
<ul>
{products.map((product) => (
<ProductItem key={product} name={product} />
))}
</ul>
);
}
export default Products;Тепер кожен компонент ProductItem має власний окремий стан.
Неправильно:
function Page() {
function createTitle() {
const [title, setTitle] = useState("Головна сторінка");
return title;
}
return <h1>{createTitle()}</h1>;
}createTitle — звичайна функція, а не React-компонент і не власний Hook. React не може очікувати на виклики Hooks у такій функції.
Правильно:
import { useState } from "react";
function Page() {
const [title, setTitle] = useState("Головна сторінка");
return (
<div>
<h1>{title}</h1>
<button onClick={() => setTitle("Нова сторінка")}>
Змінити заголовок
</button>
</div>
);
}
export default Page;Обробник події може викликати функції, змінювати стан і виконувати інші дії, але не повинен викликати Hooks:
function Form() {
const [value, setValue] = useState("");
function handleSubmit() {
// Тут можна виконати звичайну логіку форми
console.log(value);
}
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<button type="submit">Надіслати</button>
</form>
);
}Сам Hook потрібно викликати на верхньому рівні, але умови можна розміщувати всередині його аргументів або функції, яку він приймає.
Наприклад, useEffect викликається завжди, але його внутрішня логіка може залежати від умови:
import { useEffect, useState } from "react";
function Status({ isOnline }) {
const [message, setMessage] = useState("Перевірка статусу...");
useEffect(() => {
if (isOnline) {
setMessage("Користувач онлайн");
} else {
setMessage("Користувач офлайн");
}
}, [isOnline]);
return <p>{message}</p>;
}
export default Status;У цьому прикладі:
useEffect викликається під час кожного рендера;
умова знаходиться всередині функції ефекту;
ефект повторно виконується, коли змінюється isOnline.
Правильно:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}Неправильно:
import { useState } from "react";
function getInitialValue() {
const [value, setValue] = useState(0);
return value;
}Звичайна функція getInitialValue не є компонентом React. Якщо функції потрібен стан, варто перетворити її на компонент або власний Hook.
Власний Hook — це функція, яка:
починається з use;
може викликати інші Hooks;
використовується у функціональних компонентах або інших власних Hooks.
Наприклад, логіку лічильника можна винести у власний Hook:
import { useState } from "react";
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
function increment() {
setCount((currentCount) => currentCount + 1);
}
function decrement() {
setCount((currentCount) => currentCount - 1);
}
return {
count,
increment,
decrement,
};
}
function Counter() {
const { count, increment, decrement } = useCounter(10);
return (
<div>
<p>Значення: {count}</p>
<button onClick={decrement}>−</button>
<button onClick={increment}>+</button>
</div>
);
}
export default Counter;useCounter є власним Hook, тому він може викликати useState. Компонент Counter використовує цей Hook на верхньому рівні.
Не викликайте власні Hooks в умовах:
function App({ enabled }) {
if (enabled) {
const counter = useCounter();
}
return <div>Застосунок</div>;
}Правильно:
function App({ enabled }) {
const counter = useCounter();
return (
<div>
{enabled ? <p>Лічильник: {counter.count}</p> : <p>Лічильник вимкнено</p>}
</div>
);
}React розпізнає Hooks за їхнім порядком виклику під час рендера.
Наприклад:
function Component() {
const firstState = useState("перше значення");
const secondState = useState("друге значення");
// ...
}React пов’язує:
перший виклик useState з firstState;
другий виклик useState з secondState.
Якщо під час наступного рендера один із викликів зникне через умову, React більше не зможе правильно визначити, який стан до якого Hook належить.
Тому всі Hooks мають викликатися:
у тому самому порядку;
під час кожного рендера;
без умов, циклів і вкладених функцій.
Для автоматичної перевірки правил Hooks у React-проєктах зазвичай використовують правило react-hooks/rules-of-hooks з пакета ESLint для React Hooks.
Окрім помилок, це правило допомагає помітити неправильне розташування викликів ще до запуску застосунку. Повідомлення на кшталт React Hook "useState" is called conditionally означає, що Hook викликається не на верхньому рівні.
Неправильно:
function Profile({ user }) {
if (!user) {
return <p>Користувача не знайдено</p>;
}
const [isEditing, setIsEditing] = useState(false);
return <p>{isEditing ? "Редагування" : "Перегляд"}</p>;
}Якщо user спочатку відсутній, а потім з’явиться, порядок викликів зміниться.
Правильно:
function Profile({ user }) {
const [isEditing, setIsEditing] = useState(false);
if (!user) {
return <p>Користувача не знайдено</p>;
}
return (
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? "Редагування" : "Перегляд"}
</button>
);
}Усі Hooks потрібно розміщувати до можливого раннього return.
mapНеправильно:
function List({ items }) {
return (
<ul>
{items.map((item) => {
const [active, setActive] = useState(false);
return <li key={item}>{item}</li>;
})}
</ul>
);
}Правильний підхід — винести елемент списку в окремий компонент, як у прикладі з ProductItem.
Неправильно:
function Button() {
function handleClick() {
const [value, setValue] = useState(0);
}
return <button onClick={handleClick}>Натиснути</button>;
}Hook потрібно викликати в компоненті:
function Button() {
const [value, setValue] = useState(0);
function handleClick() {
setValue((currentValue) => currentValue + 1);
}
return <button onClick={handleClick}>{value}</button>;
}Hooks викликаються лише на верхньому рівні функціонального компонента або власного Hook.
Не викликайте Hooks в умовах, циклах і вкладених функціях.
Не викликайте Hooks у звичайних функціях або обробниках подій.
Усі Hooks повинні викликатися в однаковому порядку під час кожного рендера.
Умовну логіку розміщуйте всередині Hook або використовуйте її для відображення результату.
Якщо елемент списку має власний стан, створіть для нього окремий компонент.
Власні Hooks повинні починатися з use і можуть використовувати інші Hooks.