- Quick Start
- Installation
- Describing the UI
- Adding Interactivity
- Managing State
- Escape Hatches
GET STARTED
LEARN REACT
Спільне використання стану між компонентами
Іноді вам потрібно, щоб стани двох компонентів завжди змінювалися разом. Для цього вилучіть стан з обох компонентів, перемістіть його до їхнього найближчого спільного батька, а потім передайте його їм через пропси. Це називається підняттям стану вгору, і це одна з найпоширеніших речей, які ви будете робити під час написання React-коду.
- Як розділити стан між компонентами, піднявши його вгору
- Що таке контрольовані та неконтрольовані компоненти
Підняття стану на прикладі
У цьому прикладі батьківський Accordion
компонент рендерить два окремих Panel
s:
Accordion
Panel
Panel
Кожен Panel
компонент має булевий isActive
стан, який визначає, чи буде видно його вміст.
Натисніть кнопку Показати для обох панелей:
import { useState } from 'react';
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
Show
</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About">
With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
</Panel>
<Panel title="Etymology">
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
</Panel>
</>
);
}
h3, p { margin: 5px 0px; }
.panel {
padding: 10px;
border: 1px solid #aaa;
}
Зверніть увагу, що натискання кнопки однієї панелі не впливає на іншу панель - вони незалежні.
А тепер уявімо, що ви хочете змінити його так, щоб у будь-який момент часу розгорталася лише одна панель. За такої схеми розгортання другої панелі призведе до згортання першої. Як це можна зробити?
Щоб скоординувати ці дві панелі, потрібно "підняти їх стан" до батьківського компонента у три кроки:
- Видалення стану з дочірніх компонентів.
- Передати жорстко закодовані дані зі спільного батька.
- Додати стан до спільного батька і передати його разом з обробниками подій.
Це дозволить компоненту Accordion
координувати обидва Panel
і розширювати лише по одному за раз.
Крок 1: Видалення стану з дочірніх компонентів
Ви передасте керування Panel
батьківському компоненту isActive
. Це означає, що батьківський компонент передасть isActive
до Panel
як проп замість нього. Почніть з видалення цього рядка з компонента Panel
:
const [isActive, setIsActive] = useState(false);
Натомість додайте isActive
до списку пропсів Panel
:
function Panel({ title, children, isActive }) {
Тепер батьківський компонент Panel
може керувати isActive
, передаючи його як проп. І навпаки, компонент Panel
тепер не має жодного контролю над значенням isActive
- тепер це залежить від батьківського компонента!
Крок 2: Передача жорстко закодованих даних від спільного батька
Щоб підняти стан вгору, вам потрібно знайти найближчий спільний батьківський компонент обох дочірніх компонентів, які ви хочете скоординувати:
Accordion
(найближчий спільний батько)Panel
Panel
У цьому прикладі це компонент Accordion
. Оскільки він знаходиться над обома панелями і може керувати їхніми пропсами, він стане "джерелом істини" щодо того, яка панель наразі активна. Зробіть так, щоб компонент Accordion
передавав жорстко закодоване значення isActive
(наприклад, true
) до обох панелей:
import { useState } from 'react';
export default function Accordion() {
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About" isActive={true}>
With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
</Panel>
<Panel title="Etymology" isActive={true}>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
</Panel>
</>
);
}
function Panel({ title, children, isActive }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
Show
</button>
)}
</section>
);
}
h3, p { margin: 5px 0px; }
.panel {
padding: 10px;
border: 1px solid #aaa;
}
Спробуйте відредагувати жорстко закодовані значення isActive
у компоненті Accordion
і подивіться результат на екрані.
Крок 3: Додавання стану до спільного батька
Підняття стану вгору часто змінює природу того, що ви зберігаєте як стан.
У цьому випадку одночасно має бути активною лише одна панель. Це означає, що спільний батьківський компонент Accordion
повинен відстежувати яка з панелей є активною. Замість булевого значення
, він може використовувати число як індекс активної Panel
для змінної стану:
const [activeIndex, setActiveIndex] = useState(0);
Коли activeIndex
дорівнює 0
, активною є перша панель, а коли 1
- друга.
Натискання кнопки "Показати" у будь-якому з Panel
має змінити активний індекс у Accordion
. Panel
не може встановити стан activeIndex
безпосередньо, оскільки він визначений всередині Accordion
. Компонент Accordion
повинен явно дозволити компоненту Panel
змінювати свій стан шляхом передачі обробника події як проп:
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
<button>
всередині Panel
тепер використовуватиме проп onShow
як обробник події кліку:
import { useState } from 'react';
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel
title="About"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
</Panel>
<Panel
title="Etymology"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
</Panel>
</>
);
}
function Panel({
title,
children,
isActive,
onShow
}) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
Show
</button>
)}
</section>
);
}
h3, p { margin: 5px 0px; }
.panel {
padding: 10px;
border: 1px solid #aaa;
}
На цьому підняття стану завершено! Переміщення стану у спільний батьківський компонент дозволило скоординувати дві панелі. Використання активного індексу замість двох прапорців "is shown" гарантувало, що лише одна панель буде активною в певний момент часу. А передача обробника події дочірньому компоненту дозволила дочірньому компоненту змінювати стан батьківського.
Контрольовані та неконтрольовані компоненти
Компонент з деяким локальним станом прийнято називати "неконтрольованим". Наприклад, оригінальний компонент Panel
зі змінною стану isActive
є некерованим, оскільки його батько не може впливати на те, чи є панель активною.
На противагу цьому, можна сказати, що компонент є "контрольованим", коли важлива інформація у ньому керується пропсами, а не його власним локальним станом. Це дозволяє батьківському компоненту повністю визначати його поведінку. Кінцевий компонент Panel
з пропсом isActive
контролюється компонентом Accordion
.
Некеровані компоненти легше використовувати у складі своїх батьків, оскільки вони потребують меншої конфігурації. Але вони менш гнучкі, коли вам потрібно скоординувати їх між собою. Контрольовані компоненти є максимально гнучкими, але вони вимагають, щоб батьківські компоненти повністю налаштували їх за допомогою пропсів.
На практиці, "контрольований" та "неконтрольований" не є строгими технічними термінами - кожен компонент зазвичай має деяку суміш локального стану та реквізитів. Втім, це корисний спосіб розповісти про те, як розроблено компоненти та які можливості вони пропонують.
Під час написання компонента слід враховувати, яка інформація у ньому має бути контрольованою (через пропси), а яка - неконтрольованою (через стан). Але ви завжди можете передумати і зробити рефакторинг пізніше.
Єдине джерело істини для кожного стану
У React-застосунку багато компонентів матимуть власний стан. Деякі стани можуть "жити" ближче до листків (компонентів внизу дерева), як, наприклад, вхідні дані. Інші стани можуть "жити" ближче до вершини застосунку. Наприклад, навіть клієнтські бібліотеки маршрутизації зазвичай реалізуються шляхом зберігання поточного маршруту в стані React і передачі його далі за допомогою пропсів!
Для кожного унікального фрагмента стану ви виберете компонент, який ним "володіє". Цей принцип також відомий як наявність "єдиного джерела істини". Це не означає, що всі держави живуть в одному місці, але для кожної частини держави існує специфічний компонент, який зберігає цю частину інформації. Замість того, щоб дублювати спільний стан між компонентами, підніміть його вгору до їхнього спільного батька і передайте його вниз дочірнім компонентам, які його потребують.
Ваша програма змінюватиметься під час роботи над нею. Часто буде так, що ви переміщуватимете стан вниз або назад, поки ви все ще з'ясовуєте, де "живе" кожна частина стану. Це все частина процесу!
Щоб побачити, як це виглядає на практиці з кількома іншими компонентами, прочитайте Мислення в React.
- Коли ви хочете скоординувати два компоненти, перемістіть їхній стан до їхнього спільного батька.
- Тоді передайте інформацію через пропси від їхнього спільного батька.
- Нарешті, передайте обробники подій вниз, щоб дочірні елементи могли змінювати стан батьків.
- Корисно розглядати компоненти як "контрольовані" (керовані пропсами) або "неконтрольовані" (керовані станом).
Синхронізовані входи
Ці два входи є незалежними. Зробіть так, щоб вони синхронізувалися: редагування одного входу має оновити інший вхід тим самим текстом, і навпаки.
Вам потрібно підняти їхній стан до батьківського компонента.
import { useState } from 'react';
export default function SyncedInputs() {
return (
<>
<Input label="First input" />
<Input label="Second input" />
</>
);
}
function Input({ label }) {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<label>
{label}
{' '}
<input
value={text}
onChange={handleChange}
/>
</label>
);
}
input { margin: 5px; }
label { display: block; }
Перемістіть змінну стану text
у батьківський компонент разом з обробником handleChange
. Потім передайте їх як пропси обом компонентам Input
. Це забезпечить їхню синхронізацію.
import { useState } from 'react';
export default function SyncedInputs() {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<Input
label="First input"
value={text}
onChange={handleChange}
/>
<Input
label="Second input"
value={text}
onChange={handleChange}
/>
</>
);
}
function Input({ label, value, onChange }) {
return (
<label>
{label}
{' '}
<input
value={value}
onChange={onChange}
/>
</label>
);
}
input { margin: 5px; }
label { display: block; }
Фільтрування списку
У цьому прикладі SearchBar
має власний стан query
, який контролює введення тексту. Його батьківський компонент FilterableList
відображає List
елементів, але не враховує пошуковий запит.
Використовуйте функцію filterItems(foods, query)
для фільтрації списку відповідно до пошукового запиту. Щоб перевірити ваші зміни, переконайтеся, що введення "s" у вхідних даних фільтрує список до "Суші", "Шашлик" і "Дім сам".
Зверніть увагу, що filterItems
вже реалізовано та імпортовано, тому вам не потрібно писати його самостійно!
Вам потрібно видалити стан запиту
та обробник handleChange
з SearchBar
і перемістити їх до FilterableList
. Потім передайте їх до SearchBar
як query
та onChange
props.
import { useState } from 'react';
import { foods, filterItems } from './data.js';
export default function FilterableList() {
return (
<>
<SearchBar />
<hr />
<List items={foods} />
</>
);
}
function SearchBar() {
const [query, setQuery] = useState('');
function handleChange(e) {
setQuery(e.target.value);
}
return (
<label>
Search:{' '}
<input
value={query}
onChange={handleChange}
/>
</label>
);
}
function List({ items }) {
return (
<table>
<tbody>
{items.map(food => (
<tr key={food.id}>
<td>{food.name}</td>
<td>{food.description}</td>
</tr>
))}
</tbody>
</table>
);
}
export function filterItems(items, query) {
query = query.toLowerCase();
return items.filter(item =>
item.name.split(' ').some(word =>
word.toLowerCase().startsWith(query)
)
);
}
export const foods = [{
id: 0,
name: 'Sushi',
description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'
}, {
id: 1,
name: 'Dal',
description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'
}, {
id: 2,
name: 'Pierogi',
description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'
}, {
id: 3,
name: 'Shish kebab',
description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'
}, {
id: 4,
name: 'Dim sum',
description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'
}];
Підніміть стан запиту
у компонент FilterableList
. Викличте filterItems(foods, query)
, щоб отримати відфільтрований список і передайте його до List
. Тепер зміна вхідних даних запиту відображається у списку:
import { useState } from 'react';
import { foods, filterItems } from './data.js';
export default function FilterableList() {
const [query, setQuery] = useState('');
const results = filterItems(foods, query);
function handleChange(e) {
setQuery(e.target.value);
}
return (
<>
<SearchBar
query={query}
onChange={handleChange}
/>
<hr />
<List items={results} />
</>
);
}
function SearchBar({ query, onChange }) {
return (
<label>
Search:{' '}
<input
value={query}
onChange={onChange}
/>
</label>
);
}
function List({ items }) {
return (
<table>
<tbody>
{items.map(food => (
<tr key={food.id}>
<td>{food.name}</td>
<td>{food.description}</td>
</tr>
))}
</tbody>
</table>
);
}
export function filterItems(items, query) {
query = query.toLowerCase();
return items.filter(item =>
item.name.split(' ').some(word =>
word.toLowerCase().startsWith(query)
)
);
}
export const foods = [{
id: 0,
name: 'Sushi',
description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'
}, {
id: 1,
name: 'Dal',
description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'
}, {
id: 2,
name: 'Pierogi',
description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'
}, {
id: 3,
name: 'Shish kebab',
description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'
}, {
id: 4,
name: 'Dim sum',
description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'
}];