Підйом стану
These docs are old and won’t be updated. Go to react.dev for the new React docs.
These new documentation pages teach modern React and include live examples:
Часто кілька компонентів повинні відображати одні і ті ж змінювані дані. Ми рекомендуємо підняти спільний стан до їхнього найближчого спільного предка. Давайте подивимося, як це працює.
У цьому розділі ми створимо калькулятор температури, який обчислює, чи буде вода закипати при заданій температурі.
Почнемо з компонента під назвою BoilingVerdict
. Він приймає температуру celsius
як проп і виводить, чи її достатньо для закипання:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>Вода закипить.</p>; }
return <p>Вода не закипить.</p>;}
Далі ми створимо компонент під назвою Calculator
. Він рендерить <input>
, що дозволяє вводити температуру і зберігає її значення в this.state.temperature
.
Крім того, він рендерить BoilingVerdict
для поточного введеного значення.
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''}; }
handleChange(e) {
this.setState({temperature: e.target.value}); }
render() {
const temperature = this.state.temperature; return (
<fieldset>
<legend>Введіть температуру в градусах Цельсія:</legend>
<input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset>
);
}
}
Додавання другого поля вводу
Наша нова вимога полягає в тому, що крім поля вводу температури за Цельсієм, ми надамо можливість вводити температуру за Фаренгейтом, і ці два поля будуть синхронізовані між собою.
Ми можемо почати з того, що витягнемо компонент TemperatureInput
з Calculator
. До нього додамо проп scale
, який може бути "c"
або "f"
:
const scaleNames = { c: 'Цельсій', f: 'Фаренгейт'};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale; return (
<fieldset>
<legend>Введіть температуру в градусах {scaleNames[scale]}:</legend> <input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
Тепер ми можемо змінити Calculator
для рендеру двох окремих полів вводу температури:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div>
);
}
}
Зараз ми маємо два поля вводу, але коли ви вводите температуру в одному з них, інший не оновлюється. Це суперечить нашій вимозі: ми хочемо, щоб вони були синхронізовані.
Ми також не можемо відобразити BoilingVerdict
зCalculator
. Calculator
не знає поточну температуру, тому що вона прихована всередині TemperatureInput
.
Написання функцій перетворення
Для початку ми напишемо дві функції для перетворення температури з градусів по Цельсію у градуси по Фаренгейту і навпаки:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
Ці дві функції перетворюють числа. Ми напишемо ще одну функцію, яка приймає два аргумента: рядок temperature
і функцію перетворення convert
, і повертає рядок. Ми будемо використовувати його для обчислення значення одного поля вводу на основі іншого.
Функція повертатиме рядок, що відповідає значенню, округленому до третього числа після коми, або ж порожній рядок в разі помилкового значення temperature
:
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
Наприклад, tryConvert('abc', toCelsius)
повертає порожній рядок, а tryConvert('10.22', toFahrenheit)
повертає '50.396'
.
Підйом стану
В даний час обидва компоненти TemperatureInput
незалежно зберігають свої значення в локальному стані:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''}; }
handleChange(e) {
this.setState({temperature: e.target.value}); }
render() {
const temperature = this.state.temperature; // ...
Однак ми хочемо, щоб ці два поля вводу були синхронізовані одне з одним. Коли ми оновлюємо поле Цельсія, поле Фаренгейта повинне відображати перетворену температуру і навпаки.
Спільний доступ до стану в React здійснюється шляхом переміщення його до найближчого спільного предка компонентів, які його потребують. Це називається “підйом стану вгору”. Ми видалимо локальний стан з TemperatureInput
і перемістимо його в Calculator
.
Якщо Calculator
володіє спільним станом, він стає “джерелом істини” для поточної температури в обох полях вводу. Він може надати їм обом значення, які узгоджуються один з одним. Оскільки пропси обох компонентів TemperatureInput
приходять з одного і того ж батьківського компонента Calculator
, то їх поля вводу завжди будуть синхронізовані.
Давайте подивимося, як це працює крок за кроком.
Спершу ми замінимо this.state.temperature
на this.props.temperature
у компоненті TemperatureInput
. Наразі давайте вдамо, що this.props.temperature
вже існує, хоча в майбутньому нам доведеться передавати його з Calculator
:
render() {
// До того: const temperature = this.state.temperature;
const temperature = this.props.temperature; // ...
Ми знаємо, що пропси можна тільки читати. Коли temperature
була в локальному стані, в компоненті TemperatureInput
можна було просто викликати this.setState()
, щоб змінити її. Однак тепер, коли temperature
надходить від батьківського компонента як проп, TemperatureInput
не має контролю над ним.
У React це, як правило, вирішується шляхом створення “контрольованого” компонента. Так само, як DOM-елемент <input>
приймає атрибути value
і onChange
, так і користувацький TemperatureInput
може прийняти пропси temperature
і onTemperatureChange
зі свого батьківського компонента Calculator
.
Тепер, коли TemperatureInput
“хоче” оновити свою температуру, він викликає this.props.onTemperatureChange
:
handleChange(e) {
// До того: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value); // ...
Примітка:
Немає спеціального сенсу в іменах
temperature
абоonTemperatureChange
в користувацьких компонентах. Ми могли б назвати їх будь-як інакше. Наприклад,value
іonChange
, що є загальноприйнятими значеннями.
Проп onTemperatureChange
передасться разом з пропом temperature
від батьківського компонента Calculator
. Він буде обробляти зміни, змінюючи свій власний локальний стан, і таким чином провокувати повторний рендер обох полів вводу з новими значеннями. Незабаром ми розглянемо нову реалізацію Calculator
.
Перед тим як зануритися в зміни в Calculator
, давайте підсумуємо наші зміни компонента TemperatureInput
. Ми видалили з нього локальний стан і замість this.state.temperature
використовуємо this.props.temperature
. Замість виклику this.setState()
, коли ми хочемо внести зміни, тепер викликаємо this.props.onTemperatureChange()
, який буде отриманий від компонента Calculator
:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value); }
render() {
const temperature = this.props.temperature; const scale = this.props.scale;
return (
<fieldset>
<legend>Введіть температуру в градусах {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
Тепер перейдемо до компонента Calculator
.
Ми будемо зберігати значення temperature
і scale
у його локальному стані. Це стан, який ми “підняли” з полів вводу, і він буде слугувати “джерелом істини” для них обох. Це мінімальне представлення всіх даних, які ми повинні знати, щоб відрендерити обидва поля вводу.
Наприклад, якщо ми введемо 37 у поле за Цельсієм, стан компонента Calculator
буде:
{
temperature: '37',
scale: 'c'
}
Якщо ми пізніше змінимо значення поля за Фаренгейтом на 212, стан компонента Calculator
буде:
{
temperature: '212',
scale: 'f'
}
Ми могли б зберегти значення обох полів, але це непотрібно. Достатньо зберегти значення останнього зміненого поля вводу і шкалу, яку він представляє. Тоді ми можемо зробити висновок про значення іншого поля, виходячи з поточних temperature
і scale
.
Поля вводу залишаються синхронізованими, оскільки їх значення обчислюються з одного і того ж стану:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'}; }
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature}); }
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature}); }
render() {
const scale = this.state.scale; const temperature = this.state.temperature; const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature; const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius} onTemperatureChange={this.handleCelsiusChange} /> <TemperatureInput
scale="f"
temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} /> <BoilingVerdict
celsius={parseFloat(celsius)} /> </div>
);
}
}
Тепер, незалежно від того, яке поле ви редагуєте, this.state.temperature
іthis.state.scale
в Calculator
будуть оновлені. Одне з полів отримає значення, яке ввів користувач, а інше - значення, перераховане на його основі.
Давайте підсумуємо, що відбувається, коли ви редагуєте поля вводу:
- React викликає функцію, задану як
onChange
у DOM-елементі<input>
. У нашому випадку, це методhandleChange
у компонентіTemperatureInput
. - Метод
handleChange
у компонентіTemperatureInput
викликаєthis.props.onTemperatureChange()
з новим значенням. Його пропси, включно зonTemperatureChange
, були надані його батьківським компонентомCalculator
. - Коли він раніше був відрендерений,
Calculator
вказав, щоonTemperatureChange
компонентаTemperatureInput
за Цельсієм є методомhandleCelsiusChange
компонентаCalculator
, аonTemperatureChange
компонентаTemperatureInput
за Фаренгейтом є методомhandleFahrenheitChange
компонентаCalculator
. Таким чином, будь-який з цих двох методівCalculator
викликається в залежності від того, яке поле вводу ми редагували. - Усередині цих методів компонент
Calculator
просить React повторно зробити рендер самого себе, шляхом викликуthis.setState()
з новим введеним значенням і поточною шкалою вводу, яку ми щойно редагували. - React викликає метод
render
компонентаCalculator
, щоб дізнатися, як повинен виглядати UI. Значення обох полів вводу перераховуються на основі поточної температури і шкали. Також тут відбувається перетворення температури. - React викликає методи
render
окремих компонентівTemperatureInput
з новими пропсами, визначенимиCalculator
, і дізнається, як повинен виглядати їх UI. - React викликає метод
render
компонентаBoilingVerdict
, передаючи температуру в градусах Цельсія в якості пропу. - React DOM оновлює DOM відповідно до значень полів вводу. Поле, яке ми щойно редагували, отримує поточне значення, а інше - оновлюється до температури після перетворення.
Кожне оновлення проходить ті ж самі кроки, тому поля вводу залишаються синхронізованими.
Засвоєні уроки
Необхідно створити єдине “джерело істини” для будь-яких даних, які змінюються у додатку React. Зазвичай стан спочатку додають до компонента, який його потребує для рендеринга. Потім, якщо інші компоненти також потребують цього стану, ви можете підняти його до їхнього найближчого спільного предка. Замість того, щоб намагатися синхронізувати стан між різними компонентами, слід покладатися на потік даних вниз.
Підйом стану передбачає написання більше “шаблонного” коду, ніж при підході з двосторонньою прив’язкою даних. В результаті це надасть нам певні переваги - щоб знайти і ізолювати помилки, потрібно буде затратити менше часу. Оскільки будь-який стан “живе” в певному компоненті і сам компонент може його змінити, область пошуку помилок значно зменшується. Крім того, ви можете реалізувати будь-яку користувацьку логіку, щоб відхилити або перетворити дані, введені користувачем.
Якщо щось може бути обчислене або з пропсів, або зі стану, воно, ймовірно, не повинно бути в стані. Наприклад, замість того, щоб зберігати і celsiusValue
, і fahrenheitValue
, ми зберігаємо тільки останню редаговану temperature
і її scale
. Значення іншого поля вводу завжди може бути обчислено на їх основі у методі render()
. Це дозволяє очистити або застосувати округлення до іншого поля без втрати будь-якої точності даних, уведених користувачем.
Коли ви бачите якусь помилку в UI, ви можете скористатися React Developer Tools для перевірки пропсів і переміститися вгору по дереву, поки не знайдете компонент, відповідальний за оновлення стану. Це дозволяє відслідковувати джерело помилок: