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>
);
}
}
あたらいし要望があります、摂氏の入力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>
);
}
}
今、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'
を返します。
今や、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.temperatureを
this.props.temperature に変更します。 ゆくゆくは
Calculatorからそれをパスする必要がありますが、さしあたり、
this.props.temperature`のようにしましょう。
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
propsは参照しかできないことはご存知の通りです。 temperature
がlocal state内にあった場合, そのTemperatureInput
はthis.setState()
をコールして変更する事ができます。
しかしtemperature
はpropとして親からくるし、そのTemperatureInput
は変更できない。
Reactでは, これは通常、"controlled"のコンポーネントを作る事で解決します。
ちょうどのDOMがvalue
と onChange
の2つのpropを受け取るように、 TemperatureInput
もtemperature
とonTemperatureChange
の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);
temperature
か onTemperatureChange
のpropの名前の違いは特に意味がありません。
value
や onChange
のような慣習的な呼び方でも全然OKです。
onTemperatureChange
propはtemperature
prop と一緒に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の値をローカルステートのtemperature
と scale
に入れます。
これは"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>
);
}
}
今は、更新する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のTemperatureInput
のonTemperatureChange
は、Calculator
'shandleCelsiusChange
method, そしてFahrenheit
TemperatureInputのonTemperatureChange
は、Calculator
'shandleFahrehnheitChange
methodです。どっちのメソッドも更新したinputに依存して更新されます -
メソッドの中で
Calculator
コンポーネントは新しい入力値でthis.setState()
を呼び、再描画するか判断します -
ReactはどのようなUIか知るために
Calculator
componentのrender
メソッドを呼びます。 両方のinputsの値は現在の気温とscaleによって再計算されます。 気温の変換はここで行われる -
Reactは
TemperatureInput
コンポーネントのそれぞれのrender
メソッドをCalculator
によって指定された新しいと一緒にコールしUIを表示します。 -
Reactは 入力された値にあわせてDOMを更新します。 そのinputは更新した値を受け取り、 もう一つのinputの値を変換んして更新します。
毎回の更新で同じステップを通りinputの同期が実現します。
Reactアプリケーションで変更されるデータには単一の"真実の資源"が必要です。
通常ステートは描画のために、コンポーネントに追加されます。
その時、他のコンポーネントもまたそれを必要とした場合は、同じ共通の祖先にリフトアップできます。
異なるコンポーネント間でステートを同期させる代わりに、top-down data flowという方法もあります。
ステートのリフティングは双方向バイディングアプローチよりも、決まり文句の記述が多いですが、メリットもあります。 それはbugの発見と分離に有利な事です。ステートが幾つかのコンポーネントで"lives"で、そのコンポーネント自体で変更ができる時、表面的なバグは減らすことができます。さらに、拒否やユーザ入力を変形するロジックも実装できます。
もしなにかしらpropsかstateによって作成される場合、それはstateの中にはないほうがいいです。
例えば、celsiusValue
や fahrenheitValue
への保存に代わって、最後の変更とそのscaleを保存します。
他のinputの値はrender()
メソッドでいつも計算されます。それはユーザの入力を正確にクリアもしくは丸めをさせてくれます。
なにかUIで不具合に遭遇した場合は、React Developer Toolsを活用して原因となるコンポーネントが見つかるまで探しましょう。バグを突き止めるのに役立ちます。