11. Lifting State Up - accgetter/React GitHub Wiki

たまに、幾つかのコンポーネントに同じ変更のデータを反映したい事があります。
共有されたステートを共通の祖先にリフトアップする事をお勧めします。
このセクションでは、設定された気温で水が沸騰するか計算する気温計を作成します。
まず、BoilingVerdict と呼ばれるコンポーネントを作成します。
それは摂氏をpropとしてうけとります。そして沸騰に十分かどうか表示します。:

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

Try it on CodePen.

2つ目のinputの追加

あたらいし要望があります、摂氏の入力inputnに加えて、カ氏の入力を提供して、
さらにそれらを同時に保持したいのです。
TemperatureInputコンポーネントをCalculatorから抽出しましょう。
そして、新しく、"c""f"のどちらかになるscale propをそれに追加します。

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を2つに分けた気温の入力欄を描画するように変更できます。

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

Try it on CodePen.

今、2つのinputがあります、でも一つに気温を入力する時、もう一つは更新されません。
同時に値を保持するという要求を満たしていないですね。
BoilingVerdictも表示できません。 TemperatureInputの中に隠されているために、Calculatorは現在の気温を知る事はできません。

変換機能の実装

まず、摂氏から華氏に変更する関数を作ります。

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

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

これらの二つの関数は数値を変換します。
文字列の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

今や、2つの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;

しかしながら、 それらの2つのinputをそれぞれ同期させたいです。摂氏を更新した時、華氏にも反映しします。逆も同じように。
Reactでは、共有ステートは、コンポーネントの共通の祖先にリフトアップする事で完成する。
これを"lifting state up"と呼びます。
さあ、ローカルステートをTemperatureInputから削除してそれをCalculatorに移動してみましょう。 もしCalculatorが共有ステートを所有していると、それは両方のinputのための真の資源になります。
それは、両方にそれぞれお互いに違う値をもつよう教える事ができる。
両方のTemperatureInputのpropsは同じ親であるCalculatorコンポーネントからきて、その2つのinputはつねに同期される。 さあ、どうやって実現するか一つづつ見てみましょう。 まずは、TemperatureInput componentの中のthis.state.temperaturethis.props.temperature に変更します。 ゆくゆくはCalculatorからそれをパスする必要がありますが、さしあたり、this.props.temperature`のようにしましょう。

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

propsは参照しかできないことはご存知の通りです。 temperatureがlocal state内にあった場合, そのTemperatureInputthis.setState()をコールして変更する事ができます。
しかしtemperatureはpropとして親からくるし、そのTemperatureInputは変更できない。

Reactでは, これは通常、"controlled"のコンポーネントを作る事で解決します。
ちょうどのDOMがvalueonChange の2つのpropを受け取るように、 TemperatureInputtemperatureonTemperatureChangeのpropsを親であるCalculatorから受け取る。

Now, when the TemperatureInput wants to update its temperature, it calls this.props.onTemperatureChange: 今、TemperatureInputが気温の更新をしたいとき、 this.props.onTemperatureChangeを呼びましょう:

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

temperatureonTemperatureChange のpropの名前の違いは特に意味がありません。
valueonChange のような慣習的な呼び方でも全然OKです。

onTemperatureChangepropはtemperatureprop と一緒にCalculator componentから提供されます。
ローカルステートを更新する事で扱います。 結果として両方のinputに新しい値が反映されます。
とても簡単にCalculatorを作り直す事ができます。

Calculatorの変更をする前に、TemperatureInput componentに行う変更内容をまとめてみましょう。
ローカルステートを消して、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コンポーネントを見てみましょう。
inputの値をローカルステートのtemperaturescaleに入れます。
これは"lifted up"するステートで、"真の資源"として提供されます。
それは最小の描画で、2つのinputを表示します。

{
  temperature: '37',
  scale: 'c'
}

華氏を212にするとCalculatorのステートはこのようになります:

{
  temperature: '212',
  scale: 'f'
}

同じstateから計算されるのでそのinput は同期されます:

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

Try it on CodePen.

今は、更新するinputでは問題はありません、Calculator内のthis.state.temperature and this.state.scaleが更新します。
inputの一つはそのままで値を取得します。つまり、どのユーザもinputは保持されています。
そして他のinputの値はそれに基づいてつねに再計算されます。

ここで、inputが更新されるときに何がおこっているか整理しましょう:

  • Reactは、DOMの<input>上で、onChangeを呼ぶ。今回のケースでは TemperatureInputコンポーネント内のhandleChangeメソッドがそれです。

  • TemperatureInputコンポーネント内のhandleChangeメソッドは入力された新しい値でthis.props.onTemperatureChange()を呼び、 onTemperatureChangeをもっているそのpropsは親コンポーネントから提供される、Calculator

  • 事前に表示される時、Calculatorでは、CelsiusのTemperatureInputonTemperatureChangeは、Calculator's handleCelsiusChange method, そしてFahrenheit TemperatureInputのonTemperatureChangeは、Calculator's handleFahrehnheitChange methodです。どっちのメソッドも更新したinputに依存して更新されます

  • メソッドの中でCalculatorコンポーネントは新しい入力値でthis.setState()を呼び、再描画するか判断します

  • ReactはどのようなUIか知るためにCalculator componentのrenderメソッドを呼びます。 両方のinputsの値は現在の気温とscaleによって再計算されます。 気温の変換はここで行われる

  • ReactはTemperatureInputコンポーネントのそれぞれの render メソッドを Calculatorによって指定された新しいと一緒にコールしUIを表示します。

  • Reactは 入力された値にあわせてDOMを更新します。 そのinputは更新した値を受け取り、 もう一つのinputの値を変換んして更新します。

毎回の更新で同じステップを通りinputの同期が実現します。

Lessons Learned

Reactアプリケーションで変更されるデータには単一の"真実の資源"が必要です。
通常ステートは描画のために、コンポーネントに追加されます。
その時、他のコンポーネントもまたそれを必要とした場合は、同じ共通の祖先にリフトアップできます。
異なるコンポーネント間でステートを同期させる代わりに、top-down data flowという方法もあります。

ステートのリフティングは双方向バイディングアプローチよりも、決まり文句の記述が多いですが、メリットもあります。 それはbugの発見と分離に有利な事です。ステートが幾つかのコンポーネントで"lives"で、そのコンポーネント自体で変更ができる時、表面的なバグは減らすことができます。さらに、拒否やユーザ入力を変形するロジックも実装できます。

もしなにかしらpropsかstateによって作成される場合、それはstateの中にはないほうがいいです。
例えば、celsiusValuefahrenheitValueへの保存に代わって、最後の変更とそのscaleを保存します。
他のinputの値はrender()メソッドでいつも計算されます。それはユーザの入力を正確にクリアもしくは丸めをさせてくれます。

なにかUIで不具合に遭遇した場合は、React Developer Toolsを活用して原因となるコンポーネントが見つかるまで探しましょう。バグを突き止めるのに役立ちます。

Monitoring State in React DevTools
⚠️ **GitHub.com Fallback** ⚠️