Пошук уроків, статей та іншого контенту
Навчимося обчислювати значення з наявних props і State та уникати збереження зайвого Derived State.
Derived State — це значення, яке можна обчислити на основі вже наявних props і state.
Наприклад:
кількість товарів у кошику;
загальна сума замовлення;
відфільтрований список;
чи є форма валідною;
повне ім’я з імені та прізвища;
чи активна кнопка залежно від кількох умов.
Такі значення не потрібно зберігати в окремому стані, якщо їх можна отримати під час рендерингу.
const fullName = `${firstName} ${lastName}`;fullName є похідним значенням: воно залежить від firstName і lastName.
Розглянемо компонент, який зберігає список товарів і текст пошуку:
function ProductList({ products }) {
const [search, setSearch] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products);
// ...
}На перший погляд, filteredProducts здається зручним. Але він дублює інформацію, яку можна отримати з products і search.
Через це потрібно вручну синхронізувати стан:
useEffect(() => {
const nextProducts = products.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
);
setFilteredProducts(nextProducts);
}, [products, search]);Такий підхід створює додаткові проблеми:
Після зміни search компонент спочатку відрендериться зі старим filteredProducts.
Потім спрацює useEffect.
Виконається ще один рендер із новим значенням.
Якщо забути залежність у масиві useEffect, дані можуть стати неактуальними.
Одна й та сама інформація зберігається у двох місцях.
Якщо значення можна обчислити з наявних даних, краще не зберігати його в state.
Замість filteredProducts можна безпосередньо обчислювати результат:
function ProductList({ products }) {
const [search, setSearch] = useState('');
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Пошук товарів"
/>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}Тепер джерелами даних є лише:
products — props;
search — state.
filteredProducts обчислюється з них під час кожного рендерингу. Він завжди відповідає актуальним значенням.
Для кожного значення варто мати одне джерело істини — single source of truth.
Наприклад, якщо в компоненті є:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');Не потрібно додатково зберігати:
const [fullName, setFullName] = useState('');Тепер fullName може втратити синхронізацію з іншими значеннями. Краще обчислювати його:
const fullName = `${firstName} ${lastName}`.trim();Компонент має зберігати тільки ті дані, які користувач або подія змінюють безпосередньо.
У кошику кількість товарів і загальна сума є похідними значеннями.
Зберігати потрібно сам список товарів:
const [cartItems, setCartItems] = useState([
{ id: 1, name: 'Клавіатура', price: 2500, quantity: 1 },
{ id: 2, name: 'Миша', price: 1200, quantity: 2 },
]);А ці значення можна обчислити:
const totalItems = cartItems.reduce(
(sum, item) => sum + item.quantity,
0
);
const totalPrice = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);Повний приклад:
import { useState } from 'react';
const initialItems = [
{ id: 1, name: 'Клавіатура', price: 2500, quantity: 1 },
{ id: 2, name: 'Миша', price: 1200, quantity: 2 },
];
export default function ShoppingCart() {
const [cartItems, setCartItems] = useState(initialItems);
const totalItems = cartItems.reduce(
(sum, item) => sum + item.quantity,
0
);
const totalPrice = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
function increaseQuantity(id) {
setCartItems((items) =>
items.map((item) =>
item.id === id
? { ...item, quantity: item.quantity + 1 }
: item
)
);
}
function decreaseQuantity(id) {
setCartItems((items) =>
items
.map((item) =>
item.id === id
? { ...item, quantity: item.quantity - 1 }
: item
)
.filter((item) => item.quantity > 0)
);
}
return (
<section>
<h2>Кошик</h2>
{cartItems.length === 0 ? (
<p>Кошик порожній</p>
) : (
<ul>
{cartItems.map((item) => (
<li key={item.id}>
{item.name}: {item.quantity} × {item.price} грн
<button
type="button"
onClick={() => decreaseQuantity(item.id)}
>
−
</button>
<button
type="button"
onClick={() => increaseQuantity(item.id)}
>
+
</button>
</li>
))}
</ul>
)}
<p>Кількість товарів: {totalItems}</p>
<p>Загальна сума: {totalPrice} грн</p>
</section>
);
}У цьому прикладі:
cartItems — стан;
totalItems — похідне значення;
totalPrice — похідне значення;
кнопки змінюють лише cartItems;
підсумки автоматично перераховуються після кожної зміни.
Похідне значення може залежати не лише від state, а й від props.
function UserGreeting({ user }) {
const displayName = user.firstName || user.email;
return <h1>Вітаємо, {displayName}!</h1>;
}displayName не потрібно зберігати в стані, тому що він повністю визначається об’єктом user.
Інший приклад:
function OrderSummary({ items, discount }) {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const total = subtotal - subtotal * discount;
return (
<div>
<p>Проміжна сума: {subtotal} грн</p>
<p>До сплати: {total} грн</p>
</div>
);
}Якщо items або discount зміняться, компонент автоматично отримає новий результат.
useMemoЗазвичай похідні значення можна обчислювати безпосередньо під час рендерингу. Але інколи обчислення є дорогим: наприклад, потрібно відфільтрувати або відсортувати дуже великий список.
У такому випадку можна використати useMemo:
import { useMemo, useState } from 'react';
function ProductList({ products }) {
const [search, setSearch] = useState('');
const filteredProducts = useMemo(() => {
const normalizedSearch = search.trim().toLowerCase();
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedSearch)
);
}, [products, search]);
return (
<div>
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Пошук"
/>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}useMemo кешує результат і повторно обчислює його лише тоді, коли змінюються products або search.
Важливо: useMemo не перетворює похідне значення на окремий стан. Це лише оптимізація обчислення.
Не варто використовувати useMemo для кожного простого виразу:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);Для такого обчислення useMemo не потрібен:
const fullName = `${firstName} ${lastName}`;Іноді значення потрібно обчислити не для відображення, а в обробнику події. Його також не обов’язково зберігати в стані.
function Checkout({ items }) {
function handleSubmit(event) {
event.preventDefault();
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
console.log('Замовлення на суму:', total);
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Оформити замовлення</button>
</form>
);
}Тут total потрібен лише під час відправлення форми, тому зберігати його в state немає сенсу.
Зберігайте значення в state, якщо воно:
змінюється в результаті дії користувача;
не може бути однозначно відновлене з інших даних;
має зберігатися між рендерами;
є незалежним джерелом даних.
Наприклад, станом є:
const [search, setSearch] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedProductId, setSelectedProductId] = useState(null);Ці значення не є простими результатами обчислення з інших станів.
Водночас такі значення зазвичай є похідними:
const hasProducts = products.length > 0;
const isFormValid = email.includes('@') && password.length >= 8;
const selectedProduct = products.find(
(product) => product.id === selectedProductId
);Якщо кілька значень змінюються як одна логічна сутність, часто краще зберігати базові дані в одному об’єкті, а результати обчислювати окремо.
const [form, setForm] = useState({
email: '',
password: '',
});
const isFormValid =
form.email.includes('@') && form.password.length >= 8;Не потрібно створювати окремий стан:
const [isFormValid, setIsFormValid] = useState(false);Так можна уникнути ситуації, коли form уже оновився, а isFormValid ще містить старе значення.
Погано:
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(matchesSearch));
}, [items, search]);Краще:
const filteredItems = items.filter(matchesSearch);Якщо обчислення важке, використовуйте useMemo.
Погано:
setItems(nextItems);
setTotal(nextItems.reduce(getTotal, 0));Якщо total можна отримати з items, не потрібно оновлювати його вручну:
setItems(nextItems);Після рендерингу total обчислиться з нового items.
useEffect для синхронізації простих значеньПогано:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);Краще:
const fullName = `${firstName} ${lastName}`;useEffect потрібен для синхронізації з зовнішніми системами, а не для звичайних обчислень під час рендерингу.
Погано:
const [selectedProductId, setSelectedProductId] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);Тут можуть виникнути розбіжності між двома значеннями.
Краще:
const [selectedProductId, setSelectedProductId] = useState(null);
const selectedProduct = products.find(
(product) => product.id === selectedProductId
);Поставте собі такі запитання:
Чи можна отримати це значення з наявних props і state?
Чи має воно власне незалежне джерело змін?
Що станеться, якщо базові дані зміняться?
Чи доведеться вручну синхронізувати два значення?
Чи можна просто записати це як змінну в тілі компонента?
Якщо значення повністю залежить від інших даних, найімовірніше, його не потрібно зберігати в state.
Derived State — це значення, обчислене з props і state.
Не потрібно зберігати похідні значення в окремому state.
Дублювання стану створює ризик розсинхронізації.
Обчислюйте похідні значення під час рендерингу.
Для складних обчислень можна використати useMemo.
useEffect не потрібен для синхронізації простих похідних значень.
Стан має містити лише незалежні дані, а не результати, які можна відновити.