Часто несколько компонентов должны отражать одни и те же изменения данных. Мы рекомендуем поднимать общее состояние до ближайшего общего предка. Давайте посмотрим, как это работает в действии.
В этом разделе мы создадим калькулятор температуры, который вычисляет, будет ли вода кипеть при данной температуре. Начнем с компонента, который называется BoilingVerdict. Он принимает celsius температуру в качестве опоры и печатает, достаточно ли кипятить воду:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</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>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
Добавление второго входа
Наше новое требование состоит в том, что в дополнение к входу Celsius мы предоставляем вход по Фаренгейту, и они синхронизируются. Мы можем начать с извлечения TemperatureInputкомпонента из Calculator. Мы добавим к нему новую scaleопору, которая может быть "c"или "f":
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
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>Enter temperature in {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 и функцию преобразователя в качестве аргументов и возвращает строку. Мы будем использовать его для вычисления значения одного входа на основе другого ввода.
Он возвращает пустую строку с недопустимым значением 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'.
Подъемное состояние вверх (Lifting State Up)
В настоящее время оба 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;
// ...
Однако мы хотим, чтобы эти два входа синхронизировались друг с другом. Когда мы обновляем вход Celsius, вход Fahrenheit должен отражать преобразованную температуру и наоборот.
В React состояние совместного доступа достигается путем перемещения его до ближайшего общего предка компонентов, которые в нем нуждаются. Это называется «подъем состояния вверх». Мы удалим локальное состояние из TemperatureInputи переместим его в Calculatorвместо.
Если им Calculator принадлежит общее состояние, оно становится «источником истины» для текущей температуры на обоих входах. Он может поручить им обе иметь значения, которые согласуются друг с другом. Поскольку реквизиты обоих TemperatureInput компонентов поступают из одного и того же родительского Calculator компонента, два входа всегда будут синхронизироваться.
Давайте посмотрим, как это работает шаг за шагом.
Во-первых, мы заменим this.state.temperatureс this.props.temperature в TemperatureInput компоненте. Пока давайте притворимся, что this.props.temperature уже существует, хотя нам нужно будет передать его из Calculator будущего:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
Мы знаем, что реквизит доступен только для чтения. Когда он temperature был в локальном состоянии, он TemperatureInput мог просто позвонить, this.setState() чтобы изменить его. Однако теперь, когда temperatureон исходит от родителя в качестве опоры, TemperatureInput он не контролирует его.
В React это обычно решается путем создания компонента «контролируемого». Точно так же, как DOM <input> принимает как a, так value и onChangeопору, так что пользователь может TemperatureInput принять оба temperature и onTemperatureChange реквизит от своего родителя Calculator.
Теперь, когда TemperatureInput требуется обновить его температуру, он вызывает this.props.onTemperatureChange:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
// ...
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>Enter temperature in {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 это Calculator handleCelsiusChange метод, а также onTemperatureChange из Фаренгейта TemperatureInput является Calculator handleFahrenheitChange метода. Таким образом, любой из этих двух Calculator методов вызывается в зависимости от того, какой вход мы редактировали.
- Внутри этих методов Calculatorкомпонент запрашивает, чтобы React повторно отображал себя, вызывая this.setState() с новым значением ввода и текущим масштабом введенного нами ввода.
- React вызывает метод Calculator компонента, render чтобы узнать, как должен выглядеть пользовательский интерфейс. Значения обоих входов пересчитываются исходя из текущей температуры и активной шкалы. Здесь выполняется температурное преобразование.
- React вызывает renderметоды отдельных TemperatureInput компонентов с их новыми реквизитами, указанными в Calculator. Он узнает, как должен выглядеть пользовательский интерфейс.
- React DOM обновляет DOM, чтобы соответствовать требуемым входным значениям. Вход, который мы только что редактировали, получает его текущее значение, а другой вход обновляется до температуры после преобразования.
Каждое обновление проходит через те же шаги, поэтому входы остаются в синхронизации.
Для любых данных, которые изменяются в приложении React, должен быть один «источник истины». Обычно состояние сначала добавляется к компоненту, который ему нужен для рендеринга. Затем, если другие компоненты также нуждаются в этом, вы можете поднять его до ближайшего общего предка. Вместо того, чтобы пытаться синхронизировать состояние между различными компонентами, вы должны полагаться на поток данных сверху вниз .
Подъемное состояние включает в себя написание более «шаблонного» кода, чем подходы с двусторонней привязкой, но в качестве выгоды требуется меньше усилий для поиска и изоляции ошибок. Так как любое состояние «живет» в некотором компоненте, и только этот компонент может его изменить, площадь поверхности для ошибок значительно уменьшается. Кроме того, вы можете реализовать любую пользовательскую логику для отклонения или преобразования пользовательского ввода.
Если что-то может быть получено либо из реквизита, либо из состояния, оно, вероятно, не должно находиться в состоянии. Например, вместо сохранения обоих celsiusValue и fahrenheitValue, мы сохраняем только последний отредактированный temperature и его scale. Значение другого входа всегда может рассчитываться из них в render() методе. Это позволяет нам очистить или применить округление к другому полю, не теряя при этом точности ввода.
Когда вы видите что-то не так в пользовательском интерфейсе, вы можете использовать инструменты разработки React для проверки реквизита и перемещения по дереву до тех пор, пока не найдете компонент, ответственный за обновление состояния.
0 комментариев