state のリフトアップ

公式サイトのURLは https://ja.reactjs.org/docs/lifting-state-up.html

複数のコンポーネントが同一の変化するデータを反映する必要がある場合は、最も近い共通の祖先コンポーネントへ共有されている state をリフトアップすること。

えられた温度で水が沸騰するかどうかを計算する温度計算ソフトの作成を通して、この手法を学ぶ。

  • BoilingVerdict コンポーネントを作成
  • 温度 celsiusprops から受け取る
  • 水が沸騰するのに十分な温度か判定表示する
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 に保持
  • 現在の入力値を判定する Boilingdict をレンダー
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>
    );
  }
}

2 つ目の入力を追加する

  • 温度として華氏を受け取れるようにする
  • Calculator クラスから TemperatureInput コンポーネントを抽出
  • props として c もしくは f の値をとる scale を追加
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>
    );
  }
}

2つの別個の温度入力フィールドをレンダーするように変更する。

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

2つの入力フィールドが用意できた。これあの入力は連動していません。 Calculator クラスは、 TemperatureInput の温度を知らないからです。

変換関数の作成

  • 摂氏から華氏に変換する関数を作成
  • 華氏から摂氏に変換する関数を作成
function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

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

-文字列 temperature と変換関数を引数に取り文字列を返す関数を作成。小数第 3 位までで四捨五入し、無効な 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();
}

公式サイトの例をデベロッパーツールの Console タブに入力する。

tryConvert('10.22', toFahrenheit)
"50.396"
tryConvert('abc', toCelsius)
""

stateのリフトアップ

すごく丁寧に解説されている。丁寧なあまりまわりくどくて要点をつかむのに苦労するのは、日本語力も低い私だけだろな。

React は単方向データバインディング。では、複数のコンポーネントを連携して、一方の状態の変化に応じて、他方の状態を変更できないのか?

いや、相互に連携して状態を変更する方法はある。複数のコンポーネントが所属する親要素から制御すればよいのです。その方法が述べられている。

reder()props.temperature を渡します。 state から props に書き換えたことで何が変わるのか?

state : そのコンポーネントが持っている状態 props : 親コンポーネントから渡されたプロパティ

render() {
  const temperature = this.props.temperature;
  // const temperature = this.state.temperature;

まさにリフトアップしたのです。これで親要素から制御する準備が整いました。しかし、 props は読み取り専用です。新たな問題が発生しました。

TemperatureInput は、DOM の <input>valueonChange プロパティの両方を受け取るように、 親コンポーネントから値とイベントを受け取れる。

render() {
handleChange(e) {
  this.props.onTemperatureChange(e.target.value);
  // this.setState({ temperature: e.target.value });
}

TemperatureInput はここまでの変更を反映し次のようになっています。

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    // this.state = { temperature: '' };
  }
  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
    // this.setState({ temperature: e.target.value });
  }
  render() {
    const temperature = this.props.temperature;
    // 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>
    );
  }
}

<fieldset> 要素は HTML フォームの一部をグループ化し、内側の <legend> 要素で <fieldset> のキャプションを提供しています。

Calculator は同じ state から算出され、入力コンポーネントは常に同期する。

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>
    );
  }
}

この章で学んだこと

理解できれば学べたはずの事柄。

  • 変化するデータは単一の"信頼出来る情報源"であるべき
  • 共通祖先コンポーネントにリフトアップすべし
  • state は「トップダウン型データフロー」で設計する
  • リフトアップはバグを減らす

state のリフトアップを学びました。種明かしされればなるほどと理解できたように錯覚してしまいます。

実際にリフトアップが必要なケースになったら何度も読み直すことになること間違いなしです。

全体のコードを知りたい場合は、 Try i on CodePen で知ることができる。

頭が疲れた。今回は短いですが、ここで区切り。