ReactのLifting State Upをひとつずつ

はじめに

Reactの公式ドキュメントのLifting State Up(Lifting State Up - React)が若干分かりにくかったので,別のコードで実装し直してみます.
使いどころが多そうなので今後のためにもメモを残しておきます.

Lifting State Up

Lifting State Up(Stateを持ち上げる)とは,いくつかの子コンポーネントで共通のデータを扱う必要があるときに,それを親コンポーネントに持ち上げた上で管理してもらう方法です.
よく分かりにくいので,例を挙げて示します.

Example

キロメートル/メートル変換ツール

例えば,キロメートル(kilometer)とメートル(meter)を相互変換するようなツールを考えます.
値の入力には,以下のようなフォームとイベントハンドラを合わせたコンポーネント(DistanceForm)を利用するとします.

import React from 'react';
import ReactDOM from 'react-dom';

class DistanceForm extends React.Component {
    constructor(props) {
        super(props);

        this.onChange = this.onChange.bind(this);

        // コンポーネントが管理するフォームのvalue
        this.state = {
            value: 0
        }
    }

    // フォームの値が変更された際に呼び出されるイベントハンドラ
    onChange(e) {
        this.setState({ value: parseInt(e.target.value) });
    }

    render() {
        return (
            <input
                type="text"
                onChange={this.onChange}
                name={this.props.name}
                value={this.state.value}
            />
        )
    }
}

const element = (
    <div>
        <DistanceForm name="kilometer" />
        <DistanceForm name="meter" />
    </div>
)

ReactDOM.render(
    element,
    document.getElementById("root")
)

ここで問題が発生します.
それぞれを別のコンポーネントとして管理してしまうと,片方の状態の変更をもう片方に伝播する方法がありません.
f:id:Szarny:20180115235534p:plain

これを解決する方法がStateの持ち上げです.
具体的には,以下のような手順でStateを持ち上げることで,片方の状態の変更がもう片方に伝播するようにします.

その1 子コンポーネントを内包する親コンポーネントを定義する

それぞれのコンポーネントの状態の変更を管理するメタなコンポーネントを定義します.
ここでは,DistanceFrameという名前にします.
f:id:Szarny:20180116001210p:plain

その2 子コンポーネント内で管理している状態を親コンポーネントで共通的に管理する

それぞれのコンポーネントが個別に状態(state)をもっていると,状態の共有ができません.
そのため,親コンポーネントでまとめて管理することにします.
f:id:Szarny:20180116001552p:plain

その3 親コンポーネントのpropsにイベントハンドラを設定する

その2において,状態の管理を親コンポーネントでまとめてすることにしたので,状態を更新するためのイベントハンドラも親コンポーネントで管理することにします.
このイベントハンドラの処理内容は,setState関数を用いて,stateを更新することです.
フォームの内容の変更を処理するイベントハンドラなので,名前はhandleChangeとしておきます.
f:id:Szarny:20180116002438p:plain

その4 子コンポーネントのpropsに親コンポーネントイベントハンドラを設定する

コンポーネントのpropsに親コンポーネントイベントハンドラを設定します.
そして,子コンポーネントが管理するフォームタグのonChange属性に,この親から渡されたイベントハンドラを設定します.
こうすることで,子コンポーネントのフォームタグの内容が変化したときに,親コンポーネントイベントハンドラが呼び出されるようになります.
f:id:Szarny:20180116002940p:plain

その5 親コンポーネントにrender関数を追加する

コンポーネントに子コンポーネントを含めてレンダリングするrender関数を追加します.
f:id:Szarny:20180116003234p:plain

結果

これでどうなるでしょうか.
コンポーネントであるmeterコンポーネントのフォームの値が変更された時の流れを考えてみます.
すると,以下のようになるはずです.

  1. 値が変更され,propsで渡された親コンポーネントイベントハンドラが呼び出される
  2. コンポーネントのstateが変化する
  3. stateの変化を観測したReactは,render関数を再度実行する
  4. コンポーネントのstateが,propsを通して子コンポーネントに伝播される

f:id:Szarny:20180116005344p:plain

これで,当初の目的であったコンポーネント間での状態の共有」が達成できました!

実装にあたっては,キロメートルとメートルの相互変換とかデータフォーマットチェックとかが必要ですが,本記事の目的とは関係ないので省略します.

実装

以上の手順を実装した結果が以下になります.

デモ

f:id:Szarny:20180116005057g:plain

ソースコード

class DistanceFrame extends React.Component {
    constructor(props) {
        super(props);

        this.handleChange = this.handleChange.bind(this);

        // 親コンポーネントで管理する状態
        this.state = {
            distance: 0,
            type: "kilometer"
        };
    }

    // 子コンポーネント内で発生するイベントのハンドラ
    handleChange(e) {
        this.setState({
            distance: parseInt(e.target.value),
            type: e.target.name
        });
    }

    convertDistance(distance, type) {
        let kilometer;
        let meter;

        if (Number.isNaN(distance)) {
            return [0, 0];
        }

        if (type === "kilometer") {
            kilometer = distance;
            meter = distance * 1000;
        } else {
            kilometer = distance / 1000;
            meter = distance;
        }

        return [kilometer, meter];
    }

    render() {
        let kilometer;
        let meter;

        [kilometer, meter] = this.convertDistance(this.state.distance, this.state.type);

        return (
            <div>
                <label>Kilometer:</label>
                <DistanceForm
                    onChange={this.handleChange}
                    name="kilometer"
                    value={kilometer}
                />
                <br />
                <label>Meter:</label>
                <DistanceForm
                    onChange={this.handleChange}
                    name="meter"
                    value={meter}
                />
            </div>
        );
    }
}

class DistanceForm extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <input
                type="text"
                onChange={this.props.onChange}
                name={this.props.name}
                value={this.props.value}
            />
        );
    }
}

ReactDOM.render(
    <DistanceFrame />,
    document.getElementById("root")
);

おわりに

ReactのLifting State Upについてまとめました.
割と使いどころが多そうなので,今のうちにマスターしておきたいです.