Spec-Zone.ru › React 17

Поднятие состояния вверх

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

В этом разделе мы создадим калькулятор температуры, который рассчитывает, будет ли вода кипеть при заданной температуре.

Мы начнём с компонента, называемого 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>
    );
  }
}

Попробовать на CodePen

Добавление второго ввода

Наша новая задача заключается в том, чтобы помимо ввода в градусах Цельсия добавить ввод в градусах Фаренгейта и поддерживать их синхронизацию.

Мы можем начать с выделения компонента 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>
    );
  }
}

Попробовать на CodePen

Теперь у нас есть два ввода, но при вводе температуры в один из них другой не обновляется. Это противоречит нашим требованиям: мы хотим поддерживать их синхронизацию.

Мы также не можем отобразить 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'.

Поднятие состояния вверх

В настоящее время оба компонента 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() {
    // Before: 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) {
    // Before: 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>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>
    );
  }
}

Попробовать на CodePen

Теперь, независимо от того, какой вход вы редактируете, this.state.temperature и this.state.scale в Calculator обновляются. Один из входов получает значение как есть, поэтому любой ввод пользователя сохраняется, а значение другого ввода всегда пересчитывается на основе него.

Давайте подведём итоги тому, что происходит при редактировании ввода:

  • React вызывает функцию, указанную как onChange на DOM-элементе <input>. В нашем случае это метод handleChange в компоненте TemperatureInput.
  • Метод handleChange в компоненте TemperatureInput вызывает this.props.onTemperatureChange() с новым желаемым значением. Его свойства, включая onTemperatureChange, были предоставлены родительским компонентом, Calculator.
  • При предыдущем отображении Calculator указал, что onTemperatureChange Цельсия — метод Calculator и onTemperatureChange Фаренгейта — метод Calculator . Таким образом, один из этих двух методов Calculator вызывается в зависимости от того, какой вход мы отредактировали.
  • Внутри этих методов компонент Calculator просит React перерисовать себя, вызвав this.setState() с новым значением ввода и текущей шкалой ввода, который мы только что отредактировали.
  • React вызывает метод Calculator компонента render для определения того, как должен выглядеть пользовательский интерфейс. Значения обоих входов пересчитываются на основе текущей температуры и активной шкалы. Здесь выполняется преобразование температуры.
  • React вызывает методы render отдельных компонентов TemperatureInput со своими новыми свойствами, заданными Calculator. Он узнаёт, как должен выглядеть их пользовательский интерфейс.
  • React вызывает метод render компонента BoilingVerdict, передавая температуру в Цельсии в качестве его свойств.
  • React DOM обновляет DOM, чтобы отобразить вывод о кипении и соответствовать желаемым значениям ввода. Вход, который мы только что отредактировали, получает своё текущее значение, а другой вход обновляется до температуры после преобразования.

Каждое обновление проходит те же шаги, поэтому входы остаются синхронизированными.

Уроки, извлечённые из опыта

Должен быть единственный «источник истины» для любых изменяемых данных в приложении React. Обычно состояние сначала добавляется к компоненту, которому оно необходимо для отображения. Затем, если его также требуют другие компоненты, вы можете поднять его к ближайшему общему предку. Вместо попытки синхронизировать состояние между разными компонентами, вы должны полагаться на потоке данных сверху вниз.

Поднятие состояния включает в себя написание большего объёма «шаблонного» кода, чем подходы с двусторонней привязкой, но в качестве преимущества потребуется меньше работы для поиска и изоляции ошибок. Поскольку любое состояние «живёт» в некотором компоненте, и только этот компонент может его изменить, область поверхности для ошибок значительно уменьшается. Кроме того, вы можете реализовать любую пользовательскую логику для отклонения или преобразования ввода пользователя.

Если что-то можно вывести из свойств или состояния, это, вероятно, не должно быть в состоянии. Например, вместо хранения celsiusValue и fahrenheitValue мы храним только последний изменённый temperature и его scale. Значение другого ввода всегда можно вычислить из них в методе render() . Это позволяет нам очищать или применять округление к другому полю без потери точности ввода пользователя.

Если вы видите что-то не так в пользовательском интерфейсе, вы можете использовать React Developer Tools для проверки свойств и перемещения вверх по дереву, пока вы не найдёте компонент, ответственный за обновление состояния. Это позволяет отслеживать ошибки до их источника:

Monitoring State in React DevToolsПолезная ли эта страница?

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/lifting-state-up.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API