Пошук уроків, статей та іншого контенту
Розміщуйте стан якомога ближче до компонентів, які його використовують, щоб зменшити зв’язність і зайві перерендерення.
Локалізація стану — це розміщення стану якомога ближче до компонентів, які його використовують.
Якщо значення потрібне лише одному компоненту, воно має зберігатися в цьому компоненті. Якщо стан потрібен кільком сусіднім компонентам, його слід підняти до найближчого спільного батьківського компонента.
Це допомагає:
зменшити зв’язність між компонентами;
уникати передачі зайвих props;
обмежити область перерендерення;
спростити пошук і зміну логіки;
зробити компонент повторно використовуваним.
Локалізація стану не означає, що весь стан потрібно зберігати всередині найменшого можливого компонента. Важливо знайти компонент, якому стан справді належить.
Розглянемо три типові ситуації.
Наприклад, відкриття та закриття меню використовується тільки компонентом Menu.
import { useState } from "react";
export default function Menu() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Закрити меню" : "Відкрити меню"}
</button>
{isOpen && (
<ul>
<li>Профіль</li>
<li>Налаштування</li>
<li>Вихід</li>
</ul>
)}
</div>
);
}Батьківському компоненту не потрібно знати про isOpen. Він не повинен зберігати цей стан і передавати його через props, якщо сам не використовує це значення.
Іноді два компоненти мають працювати з одним значенням. Наприклад, один компонент змінює текст пошуку, а інший відображає результати.
У такому випадку стан розміщують у найближчому спільному батьківському компоненті.
import { useState } from "react";
const products = [
"Ноутбук",
"Монітор",
"Клавіатура",
"Миша",
];
function SearchInput({ value, onChange }) {
return (
<input
type="search"
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="Пошук товарів"
/>
);
}
function ProductList({ products }) {
if (products.length === 0) {
return <p>Нічого не знайдено.</p>;
}
return (
<ul>
{products.map((product) => (
<li key={product}>{product}</li>
))}
</ul>
);
}
export default function ProductSearch() {
const [query, setQuery] = useState("");
const filteredProducts = products.filter((product) =>
product.toLowerCase().includes(query.toLowerCase())
);
return (
<section>
<h1>Товари</h1>
<SearchInput value={query} onChange={setQuery} />
<ProductList products={filteredProducts} />
</section>
);
}SearchInput змінює query, а ProductList використовує результат фільтрації. Тому query належить не одному з цих компонентів, а їхньому спільному батьківському компоненту ProductSearch.
Якщо стан використовується лише в глибоко вкладеному компоненті, не потрібно піднімати його до верхнього рівня лише «про запас».
function Page() {
return (
<main>
<Article />
<Comments />
</main>
);
}
function Comments() {
return <CommentForm />;
}
function CommentForm() {
// Стан форми потрібен лише CommentForm.
// Тут може бути локальний стан полів форми.
return (
<form>
<textarea placeholder="Ваш коментар" />
<button type="submit">Надіслати</button>
</form>
);
}Якщо Page і Comments не використовують значення форми, немає сенсу передавати їм стан і обробники через кілька рівнів.
Розглянемо компонент сторінки з незалежними частинами:
import { useState } from "react";
function Page() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<main>
<button onClick={() => setIsMenuOpen((open) => !open)}>
Перемкнути меню
</button>
<Article />
<Comments />
</main>
);
}
function Article() {
return <article>Текст статті</article>;
}
function Comments() {
return <section>Коментарі</section>;
}Стан меню використовується тільки кнопкою та меню. Якщо зберігати його в Page, кожна зміна цього стану спричиняє повторний виклик Page і може зачепити всю його піддеревну структуру.
Краще розмістити стан у спеціальному компоненті:
import { useState } from "react";
function Menu() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Сховати меню" : "Показати меню"}
</button>
{isOpen && <nav>Пункти навігації</nav>}
</div>
);
}
function Page() {
return (
<main>
<Menu />
<Article />
<Comments />
</main>
);
}
function Article() {
return <article>Текст статті</article>;
}
function Comments() {
return <section>Коментарі</section>;
}
export default Page;Тепер стан меню локалізований у Menu. Компонент Page не знає, як саме працює меню, і не відповідає за його внутрішню поведінку.
Коли стан компонента змінюється, React повторно викликає цей компонент. Якщо компонент повертає дочірні компоненти, React також може повторно обробити відповідну піддеревну структуру.
Чим вище розташований стан, тим більша частина дерева може залежати від його оновлення.
Порівняйте:
function App() {
const [query, setQuery] = useState("");
return (
<>
<Header query={query} onQueryChange={setQuery} />
<LargeContent />
<Footer />
</>
);
}Якщо пошук потрібен лише в Header, стан краще перемістити туди:
function App() {
return (
<>
<Header />
<LargeContent />
<Footer />
</>
);
}
function Header() {
const [query, setQuery] = useState("");
return (
<header>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Пошук"
/>
</header>
);
}Це не гарантує, що React ніколи не перевірятиме інші компоненти, але зменшує область, у якій змінюється стан і проходить відповідна логіка.
Не кожне повторне виконання компонента є проблемою. Локалізація стану — це передусім спосіб правильно визначити відповідальність компонентів, а оптимізація перерендерень є додатковою перевагою.
Локалізація стану не означає, що компонент ніколи не має отримувати дані через props.
Батьківський компонент може зберігати стан, якщо він координує дочірні компоненти:
import { useState } from "react";
function Accordion() {
const [openSection, setOpenSection] = useState(null);
return (
<div>
<Section
title="Розділ 1"
isOpen={openSection === 1}
onToggle={() => setOpenSection(openSection === 1 ? null : 1)}
/>
<Section
title="Розділ 2"
isOpen={openSection === 2}
onToggle={() => setOpenSection(openSection === 2 ? null : 2)}
/>
</div>
);
}
function Section({ title, isOpen, onToggle }) {
return (
<section>
<button onClick={onToggle}>
{title} {isOpen ? "▲" : "▼"}
</button>
{isOpen && <p>Вміст розділу</p>}
</section>
);
}
export default Accordion;Section отримує isOpen через props, але не зберігає цей стан самостійно. Це правильно, бо Accordion має забезпечити правило: одночасно відкритий лише один розділ.
Якби кожен Section мав власний isOpen, батьківський компонент не міг би керувати взаємодією між секціями.
Для кожного стану поставте кілька запитань:
Який компонент читає це значення?
Який компонент змінює його?
Чи використовують його кілька сусідніх компонентів?
Чи потрібно батьківському компоненту знати про це значення?
Чи є стан незалежним від інших частин інтерфейсу?
Далі застосуйте правило:
один споживач — стан у цьому компоненті;
кілька компонентів — стан у найближчому спільному батьківському компоненті;
стан потрібен усьому застосунку — лише тоді розглядайте спільне або глобальне рішення.
Важливо відрізняти спільний стан від стану, який випадково зробили спільним. Якщо значення не використовується за межами компонента, піднімати його вище не потрібно.
Стан полів форми зазвичай належить компоненту самої форми або окремому компоненту поля — залежно від вимог.
import { useState } from "react";
export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(event) {
event.preventDefault();
console.log({
email,
password,
});
}
return (
<form onSubmit={handleSubmit}>
<label>
Електронна пошта
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
</label>
<label>
Пароль
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
/>
</label>
<button type="submit">Увійти</button>
</form>
);
}Стан полів не потрібно зберігати в компоненті всієї сторінки, якщо інші частини сторінки не використовують ці значення.
Якщо сторінка має показувати, наприклад, індикатор заповненості форми, тоді стан можна підняти до найближчого компонента, якому потрібна ця інформація.
Верхній компонент поступово перетворюється на місце, де зібрано стан усіх меню, форм, вкладок і модальних вікон.
Наслідки:
багато props і обробників;
складні залежності;
важко визначити відповідальність компонентів;
зайві оновлення великих частин дерева.
Піднімайте стан лише тоді, коли це потрібно для взаємодії між компонентами.
Не варто зберігати одне й те саме значення в кількох компонентах:
// Небажаний підхід
const [selectedId, setSelectedId] = useState(null);
// У дочірньому компоненті зберігається ще один selectedIdДва джерела правди можуть розійтися. Краще залишити одне значення у найближчому спільному батьківському компоненті й передавати його через props.
Якщо значення передається через кілька рівнів лише для того, щоб дістатися до віддаленого компонента, це може свідчити про неправильне розташування стану.
Спочатку перевірте, чи можна перемістити стан ближче до його споживачів. Якщо ні, тоді вже потрібно оцінювати інші способи передавання даних.
Не кожен стан потрібно одразу робити спільним або виносити в окрему абстракцію. Почніть із локального стану, а піднімайте його лише після появи реальної потреби.
Якщо значення можна обчислити з уже наявних props або стану, зазвичай не потрібно зберігати його окремо.
function ProductList({ products, query }) {
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}filteredProducts є похідним значенням. Його не потрібно дублювати в окремому стані, якщо його можна обчислити під час рендерення.
Під час проєктування компонента:
Випишіть значення, які мають змінюватися.
Для кожного значення визначте всі компоненти-споживачі.
Знайдіть найближчого спільного предка цих компонентів.
Розмістіть стан у ньому.
Передайте значення та обробники лише компонентам, яким вони потрібні.
Не дублюйте похідні дані в стані.
Якщо згодом з’являться нові споживачі, перегляньте розташування стану.
Локалізуйте стан у компоненті, який відповідає за його поведінку.
Якщо стан потрібен кільком компонентам, розміщуйте його в найближчому спільному батьківському компоненті.
Не піднімайте стан до верхнього рівня без конкретної потреби.
Не дублюйте одне значення у кількох станах.
Похідні дані краще обчислювати, а не зберігати окремо.
Локалізація стану зменшує зв’язність і допомагає обмежити область оновлень.