Часто несколько компонентов должны отражать одни и те же изменения данных. Мы рекомендуем поднимать общее состояние до ближайшего общего предка. Давайте посмотрим, как это работает в действии.

В этом разделе мы создадим калькулятор температуры, который вычисляет, будет ли вода кипеть при данной температуре. Начнем с компонента, который называется 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 для проверки реквизита и перемещения по дереву до тех пор, пока не найдете компонент, ответственный за обновление состояния.