Пошук уроків, статей та іншого контенту
Визначимо, коли State справді потрібен, а коли достатньо props, локальних змінних, похідних значень або URL.
State потрібен не для будь-якого значення, яке використовується компонентом. Він потрібен лише тоді, коли одночасно виконуються дві умови:
значення може змінитися протягом життя компонента;
після зміни React має виконати повторний рендер.
Якщо значення можна отримати з інших даних, зберігати його окремо не потрібно. Якщо значення має надходити від батьківського компонента, його краще передати через props. Якщо воно належить навігації та має бути доступним після перезавантаження сторінки, доречніше зберігати його в URL.
Корисне запитання перед додаванням useState:
Чи є це самостійним джерелом істини, чи лише іншою формою вже наявних даних?
props підходять для даних, якими володіє батьківський компонент або інший зовнішній власник.
function UserCard({ user }) {
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
</article>
);
}
function UsersPage({ users }) {
return (
<section>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</section>
);
}UserCard не повинен створювати власну копію user у state. Його завдання — відобразити актуальний props.user.
Якщо батьківський компонент передасть нового користувача, дочірній компонент має показати нові дані автоматично.
Локальна змінна підходить для проміжного значення, яке потрібне лише під час поточного рендеру або виконання обробника.
function Price({ amount, discount }) {
const discountedAmount = amount - amount * discount;
return <strong>{discountedAmount} грн</strong>;
}discountedAmount не потрібно зберігати у state. Воно повністю визначається через amount і discount.
Локальна змінна:
створюється заново під час кожного рендеру;
не зберігається між рендерами;
сама по собі не запускає повторний рендер.
Похідне значення — це дані, які можна обчислити з props, state або інших доступних значень.
function ProductList({ products, query }) {
const normalizedQuery = query.trim().toLowerCase();
const visibleProducts = products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
return (
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Немає потреби зберігати visibleProducts у state, оскільки він залежить від products і query.
Поганий варіант:
function ProductList({ products, query }) {
const [visibleProducts, setVisibleProducts] = useState([]);
useEffect(() => {
setVisibleProducts(
products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
)
);
}, [products, query]);
// ...
}У цьому варіанті створено додаткове джерело істини:
products і query вже містять усю необхідну інформацію;
visibleProducts може тимчасово бути застарілим;
потрібен додатковий рендер після setVisibleProducts;
з’являється ризик помилок у масиві залежностей.
Для дорогих обчислень можна застосувати useMemo, але це оптимізація продуктивності, а не спосіб зробити похідне значення окремим state.
const visibleProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
}, [products, query]);useMemo не змінює правила володіння даними: visibleProducts усе одно залишається похідним значенням.
URL підходить для стану, який має:
зберігатися після перезавантаження сторінки;
копіюватися разом із посиланням;
підтримувати навігацію браузера «назад» і «вперед»;
бути доступним безпосередньо з адреси сторінки.
Типові приклади:
пошуковий запит;
активний фільтр;
номер сторінки;
вибрана вкладка;
ідентифікатор відкритого ресурсу.
Те саме значення може бути state в одному сценарії та частиною URL в іншому. Наприклад, пошуковий текст у внутрішній панелі може бути локальним state, а пошук у каталозі — значенням URL, якщо ним потрібно ділитися.
State доречний, коли значення є локальним станом поведінки компонента:
відкрито чи закрито меню;
яка вкладка активна;
чи показувати помилку форми;
значення контрольованого поля, якщо воно не має бути в URL;
поточний крок локального майстра;
стан тимчасової взаємодії.
Наприклад, стан відкриття деталей товару не обов’язково має бути в URL:
function ProductCard({ product }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<article>
<h2>{product.name}</h2>
<button onClick={() => setIsExpanded((current) => !current)}>
{isExpanded ? "Сховати опис" : "Показати опис"}
</button>
{isExpanded && <p>{product.description}</p>}
</article>
);
}isExpanded:
змінюється через взаємодію користувача;
не можна обчислити з product;
належить конкретній картці;
повинен спричиняти рендер.
Це хороший кандидат для локального state.
Якщо значення однозначно визначається іншими даними, обчислюйте його під час рендеру.
function OrderSummary({ items }) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const itemsCount = items.reduce(
(count, item) => count + item.quantity,
0
);
return (
<p>
Товарів: {itemsCount}, сума: {total} грн
</p>
);
}total і itemsCount не є незалежним станом замовлення.
Не копіюйте props у state без чіткої причини.
Поганий варіант:
function Profile({ user }) {
const [name, setName] = useState(user.name);
return <h2>{name}</h2>;
}Якщо user.name зміниться, name не оновиться автоматично. Компонент показуватиме копію старого значення.
Кращий варіант для відображення:
function Profile({ user }) {
return <h2>{user.name}</h2>;
}Окремий state може бути виправданим, якщо компонент навмисно створює незалежну чернетку. Наприклад, форма редагування може завантажити початкові дані користувача, а потім дозволити змінювати локальні поля до натискання кнопки «Зберегти». У такому разі це вже не копія для відображення, а окрема чернетка з власним життєвим циклом.
Не кожне змінне значення має бути частиною рендеру.
function CopyButton({ text }) {
function handleClick() {
const message = `Скопійовано символів: ${text.length}`;
navigator.clipboard.writeText(text);
console.log(message);
}
return <button onClick={handleClick}>Копіювати</button>;
}message потрібне лише під час виконання handleClick. Зберігати його у state немає сенсу.
Посилання на DOM-елемент, ідентифікатор таймера або попереднє значення, яке не повинно саме по собі викликати рендер, не є типовим UI-станом. Для таких випадків існує useRef.
Але важливо не використовувати useRef як спосіб приховати дані, які мають відображатися в інтерфейсі. Зміна ref.current не спричиняє рендер.
У цьому прикладі:
список товарів приходить через props;
пошуковий запит зберігається в URL;
відфільтрований список є похідним значенням;
стан розгортання картки зберігається локально в state;
проміжні значення є локальними змінними.
import { useSyncExternalStore, useState } from "react";
import { createRoot } from "react-dom/client";
const products = [
{
id: 1,
name: "Механічна клавіатура",
description: "Клавіатура з механічними перемикачами.",
},
{
id: 2,
name: "Бездротова миша",
description: "Миша з підключенням через Bluetooth.",
},
{
id: 3,
name: "USB-C док-станція",
description: "Док-станція для підключення монітора та периферії.",
},
];
function subscribeToLocation(callback) {
window.addEventListener("popstate", callback);
return () => {
window.removeEventListener("popstate", callback);
};
}
function getLocationSnapshot() {
return window.location.search;
}
function useQueryParam(name) {
const search = useSyncExternalStore(
subscribeToLocation,
getLocationSnapshot
);
const params = new URLSearchParams(search);
const value = params.get(name) ?? "";
function setValue(nextValue) {
const nextParams = new URLSearchParams(window.location.search);
if (nextValue.trim() === "") {
nextParams.delete(name);
} else {
nextParams.set(name, nextValue);
}
const nextSearch = nextParams.toString();
const nextUrl = nextSearch
? `${window.location.pathname}?${nextSearch}`
: window.location.pathname;
// Оновлюємо URL без повного перезавантаження сторінки.
window.history.replaceState(null, "", nextUrl);
// Повідомляємо React, що зовнішнє джерело змінилося.
window.dispatchEvent(new PopStateEvent("popstate"));
}
return [value, setValue];
}
function ProductCard({ product }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<li>
<h2>{product.name}</h2>
<button onClick={() => setIsExpanded((value) => !value)}>
{isExpanded ? "Сховати опис" : "Показати опис"}
</button>
{isExpanded && <p>{product.description}</p>}
</li>
);
}
function ProductSearch({ products }) {
const [query, setQuery] = useQueryParam("q");
const normalizedQuery = query.trim().toLowerCase();
const visibleProducts = products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
return (
<main>
<h1>Каталог товарів</h1>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву товару"
/>
</label>
<p>
Знайдено товарів: {visibleProducts.length}
</p>
{visibleProducts.length > 0 ? (
<ul>
{visibleProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</ul>
) : (
<p>Товарів за цим запитом не знайдено.</p>
)}
</main>
);
}
createRoot(document.getElementById("root")).render(
<ProductSearch products={products} />
);products — це props, тому ProductSearch не володіє джерелом цих даних. Батьківський код може передати інший список.
query не зберігається через useState. Джерелом істини є URL. Якщо користувач відкриє адресу з параметром ?q=mouse, компонент відразу покаже відповідний запит. Після перезавантаження значення також залишиться.
normalizedQuery — локальна змінна. Вона потрібна для поточного рендеру.
visibleProducts — похідне значення. Воно обчислюється з products і query, тому окремий state для нього не потрібен.
isExpanded — справжній локальний state. Його не можна отримати з props або URL, а зміна має змінити структуру інтерфейсу.
Для кожного нового значення послідовно перевірте:
Чи може значення бути обчислене з уже наявних даних?
Якщо так — використайте локальну змінну або useMemo для дорогого обчислення.
Чи належить значення батьківському компоненту?
Якщо так — передайте його через props.
Чи має значення бути частиною адреси, яку можна скопіювати або відновити?
Якщо так — зберігайте його в URL.
Чи це тимчасова поведінка конкретного компонента, яка впливає на рендер?
Якщо так — використайте локальний state.
Чи значення потрібне лише під час певної операції?
Якщо так — звичайної локальної змінної достатньо.
Під час проєктування важливо також визначити власника даних. Один і той самий стан не повинен без потреби дублюватися в кількох компонентах.
Проблема виникає, коли одне логічне значення зберігається одночасно в кількох місцях:
у props і локальному state;
у state і URL;
у початковому масиві та окремому масиві похідних даних.
Наприклад, якщо активний фільтр зберігається і в filter, і в filteredProducts, компоненти можуть побачити різні версії даних.
Краще зберігати мінімальний набір незалежних значень:
function Cart({ items }) {
const [coupon, setCoupon] = useState("");
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discount = coupon === "SAVE10" ? subtotal * 0.1 : 0;
const total = subtotal - discount;
// ...
}У цьому прикладі:
items — зовнішні дані;
coupon — незалежний стан форми;
subtotal, discount і total — похідні значення.
Зберігати всі чотири значення в state не потрібно.
URL не є React state, але він може бути зовнішнім джерелом даних для компонента. Тому компонент повинен:
прочитати актуальне значення URL під час рендеру;
реагувати на навігацію назад і вперед;
оновлювати URL через History API або бібліотеку маршрутизації;
не створювати окрему копію значення без необхідності.
Параметри URL особливо корисні для стану сторінки, але не для кожної дрібної взаємодії. Наприклад, стан відкриття підказки зазвичай не має сенсу в адресі, а номер сторінки каталогу — має.
Для пошукового поля часто використовують replaceState, щоб кожен введений символ не створював окремий запис в історії браузера. Для значущої навігаційної дії може бути доречним pushState.
const [total, setTotal] = useState(0);Якщо total обчислюється з items, це зайвий стан. Він може розсинхронізуватися з кошиком.
const [value, setValue] = useState(props.value);Такий код часто з’являється автоматично, але створює значення, яке більше не реагує на зміни props. Використовуйте його лише тоді, коли справді потрібна незалежна локальна копія, наприклад чернетка редагування.
setState під час обчислення похідного значенняНе потрібно оновлювати state, щоб отримати список, суму або кількість:
useEffect(() => {
setTotal(calculateTotal(items));
}, [items]);Краще обчислити значення безпосередньо з items.
Великий об’єкт state не робить модель простішою автоматично. Якщо його поля мають різних власників або одні поля залежать від інших, структура стає складною для підтримки.
Зберігайте незалежні значення окремо або обирайте структуру, яка чітко описує одну операцію. Головне — не дублювати похідні дані.
Якщо фільтр має бути в URL, недостатньо один раз прочитати його в useState:
const [filter, setFilter] = useState(
new URLSearchParams(window.location.search).get("filter") ?? ""
);Після навігації назад або вперед filter не синхронізується автоматично. Потрібен механізм підписки на зміни URL або готовий інструмент маршрутизації.
Зміна ref.current не запускає рендер. Якщо користувач має побачити нове значення, воно повинно бути в state, props або іншому джерелі, яке спричиняє оновлення React-дерева.
Коли бачите useState, перевірте:
Чи змінюється це значення після першого рендеру?
Чи має зміна цього значення змінити UI?
Чи можна отримати його з props або іншого state?
Чи не є воно копією props?
Чи має воно бути доступним через URL?
Хто є власником цього значення?
Чи існує лише одне джерело істини?
Чи потрібне тут справді збереження між рендерами?
Якщо значення є похідним, вилучення useState зазвичай зменшує кількість коду та прибирає цілий клас проблем із синхронізацією.
props використовують для даних, якими володіє батьківський компонент.
Локальні змінні підходять для проміжних значень поточного рендеру або обробника.
Похідні значення потрібно обчислювати з джерел, а не дублювати в state.
URL підходить для стану, який має переживати перезавантаження, копіюватися та підтримувати історію навігації.
state потрібен для незалежних локальних даних, зміна яких повинна спричиняти повторний рендер.
Мінімальний state із чітким власником надійніший за кілька синхронізованих копій одного значення.
Перед додаванням useState спочатку потрібно знайти джерело істини та перевірити, чи не є нове значення похідним.