Пошук уроків, статей та іншого контенту
Порівняєте props і state, визначите їхні обов’язки та зрозумієте, де має зберігатися стан компонента.
React-компонент зазвичай працює з двома видами даних:
props — вхідні дані, які компонент отримує від батьківського компонента;
state — внутрішні дані, якими компонент керує самостійно.
Обидва види даних впливають на результат рендерингу. Коли props або state змінюються, React повторно викликає компонент і оновлює інтерфейс.
function UserCard({ name, isOnline }) {
return (
<article>
<h2>{name}</h2>
<p>{isOnline ? "У мережі" : "Не в мережі"}</p>
</article>
);
}У цьому прикладі name і isOnline — це props. Компонент отримує їх ззовні та використовує для відображення.
Props передаються компоненту як атрибути JSX:
<UserCard name="Олена" isOnline={true} />Усередині компонента вони доступні як властивості об’єкта:
function UserCard(props) {
return <h2>{props.name}</h2>;
}Або через деструктуризацію параметра:
function UserCard({ name }) {
return <h2>{name}</h2>;
}Props:
надходять від батьківського компонента;
можуть мати будь-який тип: рядок, число, boolean, масив, об’єкт або функцію;
призначені для читання;
дають змогу налаштовувати поведінку й вигляд компонента;
можуть змінюватися, якщо батьківський компонент передасть нові значення.
Компонент не має змінювати отримані props напряму:
function Product({ product }) {
// Неправильно:
// product.title = "Нова назва";
return <h2>{product.title}</h2>;
}Причина полягає не лише в правилі стилю. Об’єкт product може належати батьківському компоненту або використовуватися в інших компонентах. Його зміна всередині дочірнього компонента створює приховані побічні ефекти.
Якщо компонент має змінювати дані, він повинен повідомити про це батьківський компонент за допомогою callback-функції:
function Product({ product, onDelete }) {
return (
<article>
<h2>{product.title}</h2>
<button onClick={() => onDelete(product.id)}>
Видалити
</button>
</article>
);
}Тут onDelete також є prop. Дочірній компонент викликає функцію, але рішення про зміну даних залишається за батьківським компонентом.
State — це дані, які належать конкретному компоненту та можуть змінюватися протягом його роботи.
Для створення state у функціональному компоненті використовується хук useState:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Натискань: {count}
</button>
);
}useState(0) повертає масив із двох елементів:
поточне значення state — count;
функцію для його оновлення — setCount.
Після виклику setCount React планує повторний рендеринг компонента з новим значенням.
Так робити не слід:
function Counter() {
const [count, setCount] = useState(0);
function increment() {
// Неправильно:
// count = count + 1;
}
return <button onClick={increment}>{count}</button>;
}Значення state не змінюють напряму. Потрібно використовувати функцію-оновлювач:
setCount(count + 1);Якщо нове значення залежить від попереднього, надійніше передати функцію:
setCount((previousCount) => previousCount + 1);Такий варіант особливо важливий, коли кілька оновлень можуть відбутися до наступного рендерингу.
| Ознака | Props | State | |---|---|---| | Джерело | Батьківський компонент | Сам компонент або його логіка | | Призначення | Передати дані та поведінку | Зберегти дані, що змінюються | | Хто змінює | Батьківський компонент | Власник state через setter | | Чи можна змінювати напряму | Ні | Ні, лише через setter | | Область дії | Доступні компоненту-отримувачу | Локальні для компонента | | Приклад | title, onSubmit | isOpen, query, count |
Це не означає, що props ніколи не змінюються. Батьківський компонент може повторно відрендеритися та передати дочірньому компоненту нові props. Важливо, що дочірній компонент не керує цими значеннями напряму.
Під час проєктування компонента поставте такі запитання.
Якщо значення задає батьківський компонент, це, найімовірніше, prop:
function Greeting({ userName }) {
return <p>Вітаємо, {userName}!</p>;
}Компонент Greeting не вирішує, яке ім’я показувати. Він лише відображає отримане значення.
Якщо значення змінюється після кліку, введення тексту або іншої дії, воно зазвичай має бути state:
function SearchInput() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Пошук"
/>
);
}Якщо два компоненти повинні використовувати одне й те саме змінне значення, state зазвичай зберігають у їхньому найближчому спільному батьківському компоненті. Потім батько передає значення вниз через props, а функцію зміни — також через prop.
Цей підхід називають підняттям стану.
Розглянемо список товарів:
список products належить батьківському компоненту та передається через props;
пошуковий запит змінюється під час введення, тому є state;
стан обраності картки потрібен лише самій картці, тому він локальний;
відфільтрований список не зберігається в state, оскільки його можна обчислити з products і query.
import { useState } from "react";
import { createRoot } from "react-dom/client";
const products = [
{ id: 1, name: "Клавіатура", category: "Периферія" },
{ id: 2, name: "Монітор", category: "Екрани" },
{ id: 3, name: "Миша", category: "Периферія" },
];
function ProductCard({ product }) {
const [isFavorite, setIsFavorite] = useState(false);
return (
<li>
<strong>{product.name}</strong>
<span> — {product.category}</span>
<button
type="button"
onClick={() => setIsFavorite((previous) => !previous)}
>
{isFavorite ? "Видалити з обраного" : "Додати в обране"}
</button>
</li>
);
}
function ProductList({ products }) {
const [query, setQuery] = useState("");
const normalizedQuery = query.trim().toLowerCase();
// Відфільтрований список є похідним значенням, а не окремим state.
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery)
);
return (
<section>
<h1>Товари</h1>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву"
/>
</label>
{filteredProducts.length > 0 ? (
<ul>
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</ul>
) : (
<p>Товарів не знайдено.</p>
)}
</section>
);
}
function App() {
return <ProductList products={products} />;
}
createRoot(document.getElementById("root")).render(<App />);У цьому прикладі:
products — prop компонента ProductList;
product — prop компонента ProductCard;
query — state компонента ProductList;
isFavorite — state кожної окремої картки;
filteredProducts — похідне значення, яке обчислюється під час рендерингу.
Кожна картка має власний isFavorite. Якщо на сторінці буде три картки, у кожної буде окремий стан обраності.
Поширена помилка — зберігати в state значення, яке можна отримати з props або іншого state.
Наприклад, не потрібно створювати окремий state для повного імені:
function User({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}Якщо зберігати fullName окремо, можна отримати розсинхронізацію:
function User({ firstName, lastName }) {
// Такий state може залишитися зі старим значенням,
// коли firstName або lastName зміняться.
const [fullName, setFullName] = useState(
`${firstName} ${lastName}`
);
return <p>{fullName}</p>;
}Краще зберігати лише мінімальний набір незалежних даних, а все, що можна обчислити, отримувати під час рендерингу.
Зазвичай дані в React рухаються в одному напрямку:
батьківський компонент зберігає state;
передає поточне значення дочірньому компоненту через prop;
дочірній компонент викликає callback;
батько оновлює свій state;
нове значення знову передається вниз.
Наприклад:
import { useState } from "react";
function TextField({ value, onChange }) {
return (
<input
value={value}
onChange={(event) => onChange(event.target.value)}
/>
);
}
function ProfileForm() {
const [name, setName] = useState("");
return (
<form>
<TextField value={name} onChange={setName} />
<p>Введене ім’я: {name || "—"}</p>
</form>
);
}У цьому прикладі:
ProfileForm володіє state name;
TextField отримує значення через prop value;
TextField повідомляє про введення через prop onChange;
фактична зміна state відбувається в ProfileForm.
Так дочірній компонент залишається багаторазово використовуваним, а джерело істини розташоване в одному місці.
function Profile({ name }) {
const [localName, setLocalName] = useState(name);
return <p>{localName}</p>;
}localName отримає початкове значення лише під час створення компонента. Якщо prop name згодом зміниться, localName автоматично не синхронізується.
Якщо компоненту потрібно лише показати prop, використовуйте prop напряму:
function Profile({ name }) {
return <p>{name}</p>;
}Неправильно:
items.push(newItem);
setItems(items);Створюйте новий масив:
setItems((previousItems) => [...previousItems, newItem]);Для об’єкта використовуйте новий об’єкт:
setUser((previousUser) => ({
...previousUser,
name: "Нове ім’я",
}));Якщо значення можна обчислити з props і state, окремий state може стати причиною розсинхронізації. Спочатку перевірте, чи достатньо звичайної змінної під час рендерингу.
Якщо стан потрібен кільком дочірнім компонентам, локальний state одного з них буде недоступним для інших. Перемістіть його до найближчого спільного батька й передайте необхідні дані через props.
Props — це дані та функції, які компонент отримує від батька.
State — внутрішні змінні компонента, що змінюються через setter.
Props не можна змінювати всередині дочірнього компонента.
State не можна змінювати напряму.
Дані, що приходять ззовні, зазвичай мають бути props.
Дані, що змінюються через локальну взаємодію, зазвичай мають бути state.
Похідні значення краще обчислювати, а не дублювати в state.
Якщо стан потрібен кільком компонентам, його слід підняти до спільного батьківського компонента.
Зміни зазвичай проходять зверху вниз через props, а події — знизу вгору через callback-функції.