Как повысить свой уровень в React.js

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

В этом разделе мы создадим калькулятор температуры, который определяет, будет ли кипеть вода при заданной температуре. 
Начнем с компонента, который называется “BoilingVerdic”. Он считает температуру по Цельсию (“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.value”. Кроме того, он отображает “BoilingVerdict” для текущего значения входных данных.

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {value: ''};
  }

  handleChange(e) {
    this.setState({value: e.target.value});
  }

  render() {
    const value = this.state.value;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={value}
          onChange={this.handleChange} />
        <BoilingVerdict
          celsius={parseFloat(value)} />
      </fieldset>
    );
  }
}



Попробуйте на CodePen.

Добавление второстепенных входных данных.



Новое требование заключается в том, что, в дополнение к входным данным по Цельсию, мы добавляем входные данные по Фаренгейту, и они будут синхронизированы.
Мы можем начать путем извлечения компонента “TemperatureInput” из калькулятора. Добавим новое свойство props “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 = {value: ''};
  }

  handleChange(e) {
    this.setState({value: e.target.value});
  }

  render() {
    const value = this.state.value;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={value}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}



Теперь можно изменить калькулятор, чтобы отобразить два раздельных ввода температуры:

class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}



Попробуйте на CodePen.

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

Мы также не можем отобразить “BoilingVerdict”. 
Калькулятор не знает текущую температуру, потому что она скрыта внутри “TemperatureInput”.

Как повысить программный уровень



Во-первых, мы создадим две функции для преобразования градусов по Цельсия в градусы по Фаренгейту и обратно:

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}



Данные функции конвертируют числа. Мы создадим еще одну функцию, которая принимает значение (“value”) строки и функцию конвертера в качестве параметров и возвращает строку. Мы будем использовать ее для вычисления значения одного ввода, основанного на другом. 

Она возвращает пустую строку на недопустимое значение (“value”), и округляет выходные данные до третьего знака после запятой:

function tryConvert(value, convert) {
  const input = parseFloat(value);
  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”.
Вместо этого, он будет получать как значение (“value”), так и обработчик “onChange”  посредством свойств:

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onChange(e.target.value);
  }

  render() {
    const value = this.props.value;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={value}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}



Если нескольким компонентам необходим доступ к тому же программному уровню, это означает, что программный уровень необходимо повысить до ближайшей общей базы. В нашем случае это “Calculator”. Мы будем хранить текущие значение (“value”)  и масштаб (“scale”)  в данном программном уровене.

Мы могли бы хранить значения обоих вводов, но это ненужно. Достаточно сохранить значение последнего ввода, а также шкалу Фаренгейт-Цельсий, в котором он представлен. Тогда мы сможем определить значение другого ввода, исходя исключительно из текущего значения (“value”)  и масштаба (“scale”).

Входные данные синхронизируются, т. к. их значения вычисляются из одного и того же программного уровеня:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {value: '', scale: 'c'};
  }

  handleCelsiusChange(value) {
    this.setState({scale: 'c', value});
  }

  handleFahrenheitChange(value) {
    this.setState({scale: 'f', value});
  }

  render() {
    const scale = this.state.scale;
    const value = this.state.value;
    const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value;
    const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;

    return (
      <div>
        <TemperatureInput
          scale="c"
          value={celsius}
          onChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          value={fahrenheit}
          onChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}



Попробуйте на CodePen.

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

Что получается



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

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

В случае если что-то может быть излечено либо из свойства props или программного уровня, то, скорее всего, это не относится к программному уровню. Например, вместо того, чтобы хранить одновременно “celsiusValue” и “fahrenheitValue”, мы сохраняем только последние отредактированные значение (“value”)  и масштаб (“scale”). Из них всегда можно вычислить значение другого ввода посредством рендеринга (“render()”).Это позволяет нам выяснить или применить округление к другой сфере без потери точности во входных данных пользователя.

Если в пользовательском интерфейсе вы нашли ошибку, обратитесь к «Инструментам Разработчика React» чтобы изучить свойства и выявить компонент, который отвечает за обновление программного уровня. Это позволяет отслеживать ошибки непосредственно в их источнике: