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についてまとめました.
割と使いどころが多そうなので,今のうちにマスターしておきたいです.

2018年度の目標とか

目標Ⅰ 暗号理論の理解と実装ができるようになること

CTFのCrypt問で点が稼げるようになりたいのと,将来的な卒研とかに備えて基本的な暗号理論の理解を深めておく.
加えて,具体的な暗号化方式のアルゴリズムを自前で実装する.(ライブラリを用いる場合は,それらのコードリーディングをする)

勉強には,以下の書籍やWebサイトを用いる予定.
暗号技術のすべて(IPUSIRON)|翔泳社の本
『暗号技術入門 第3版 秘密の国のアリス』
サイモン・シン、青木薫/訳 『暗号解読―ロゼッタストーンから量子暗号まで―』 | 新潮社

必要に応じて,整数論とか代数論とか情報理論とかの参考書が必要になるかも.
おすすめがあればぜひ教えてください(´・ω・`)

目標Ⅱ 中規模Webアプリケーションを開発すること

普段アルバイトでWeb関係のことをしているので,そろそろ自分でもNode.js+各種フレームワークでそこそこの規模のWebアプリケーションを開発したい.
現在,フレームワーク(ライブラリ?)の1つであるReactの勉強をしているので,チュートリアルを何週かしたあとに,実際に開発に取り組む.

あとこれを機にクラウドサービス(AWS, Azureとか)も活用していく予定.

目標Ⅲ プログラミングコンテストの問題を解けるようにすること

少し前からAtCoderのBeginner向けのコンテストに参加しているのですが,なかなか解けない...
ので,参加しつつ,この本(プログラミングコンテスト攻略のためのアルゴリズムとデータ構造 | マイナビブックス)を買って勉強していく.

目標Ⅳ 1週間に1冊ペースで書籍を読むこと

ジャンルは問わず,参考書とかバリバリの技術書以外の書籍(新書とか)をハイペースで読んでいきたい.
読書ノート付ける習慣がなくなってから読書量がめっきり減ったので,読書ノートを付けつつやっていく.
あと,月末らへんにブログに読んだ本をまとめること.

現在,読みたい本がわりとあるが,家の床に買ったままの本が積読状態なので,まずはそこから処理していく予定.

目標Ⅴ CTFの大会で上位に入れるようにすること

そこそこ簡単な問題は解けるようになってきたけど,
複数の脆弱性を組み合わせて攻撃する問題とか,暗号系の問題とか,PwnとかBinaryとかがめちゃくちゃ弱いのでそこを何とかしていく.

まずは,ハリネズミ本と問題集のハリネズミ本を繰返しやったうえで,常設CTFで練習していく予定.
SECCONとかに限らず,外国のとか普通のチーム主催の大会にもどんどこ参加していきたい.

目標Ⅵ セキュリティキャンプ全国大会に参加すること

地方大会に何回か参加したのですが,いまだに全国大会には参加したことがないので,今年こそは参加したい.
年齢的にもそろそろまずいはず...

目標Ⅶ TOEIC 900以上をとること

最終的な目標はTOEICではなくて,英語の技術書とかWebのドキュメントとかをすらすら読めるようになりたいこと.
ですが,具体的なマイルストーンがないとやっぱりやりにくいので,TOEIC 900を目標にちょびちょび英語を勉強する.

React勉強メモ (環境構築からStateまで)

はじめに

JavaScriptのライブラリであるReactの勉強メモです.
今回は環境構築から,Stateまでのメモです.

環境構築

前提

Node.jsとnpmのバージョンは以下の通りです.

$ node -v
v6.11.3

$ npm -v
3.10.10

create-react-appのインストール

Reactアプリケーションの初期設定をしてくれるcreate-react-appをnpm経由でインストールします.

$ npm install -g create-react-app

Reactアプリケーションの初期設定

$ create-react-app appName

開発用サーバの起動

$ npm start

React

公式ドキュメントである https://reactjs.org/docs を参考に進めていきます.

ここから先,
HTMLは,appName/public/index.html
JavaScriptは,appName/src/index.jsを操作しているものとします.

index.html の中身は以下の通りです.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF=8" />
        <title>React</title>
    </head>
    <body>
        <div id="root">
        </div>
    </body>
</html>

index.js では,先頭で以下の2つのモジュールをインポートしているものとします.

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

ReactDOM.render

ReactDOM.renderを用いることで,HTML内の指定したエレメントに対してビューを挿入することができます.

ReactDOM.render(
    <h1>Hello, React!</h1>,
    document.getElementById("root")
)

JSX

Reactでは,エレメントを表現する際にJSXを用います.

JSXの外観はHTMLとほぼ同じですが,あくまでJavaScript上で評価される式として動作します.
そのためJSXは,変数に格納したり,関数の引数にしたり,返り値にしたりできます.

let element = (
    <div>
        <p>Hello, JSX!</p>
    </div>
);

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

JSXの組み込み

JSXでは,中括弧{...}を用いることで,エレメントの中身や属性にJavaScriptの値を組み込むことができます.
以下は,pエレメントにformatName関数によって整形された名前をJSXの組み込み記法を用いて組み込むデモです.

let formatName = name => name.first + " " + name.last;
let myName = {
    first: "hogehoge",
    last: "fugafuga"
};
let element = (
    <p>
        Hello, {formatName(myName)}
    </p>
);

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

Component

Reactでは,あるまとまったUIを再利用可能な形で扱うことが可能です.
このまとまったUI部品をコンポーネントと呼びます.

コンポーネントは,React.Componentクラスを継承したクラスを定義することで作成することができます.コンポーネントを定義することで,それをあたかもHTMLエレメントのように扱うことができます.

// UserInfoコンポーネントをレンダリングする例
ReactDOM.render(
    <UserInfo />,
    document.getElementById("root")
);

コンポーネント内では,そのコンポーネントが管理するUIに組み込む情報をpropsというプロパティで管理します.propsへの値の受け渡しは,以下のようにして行えます.

// UserInfoコンポーネントのpropsに,nameとageという情報を渡す例
let name = "Szarny";
let age = 20;

ReactDOM.render(
    <UserInfo name={name} age={age} />,
    document.getElementById("root")
);

コンポーネントrender関数によって,自身が管理するUIビューを返却します.
この時,propsにて管理している情報を組み込みます.

// render関数にてpropsのnameとageを組み込む例
render() {
    return (
        <p>Hello, {this.props.name} ({this.props.age} years old)</p>
    );
}

コンポーネントは,異なるコンポーネントを内包することができます.

例えば,とある投稿情報(postData)に関するUIコンポーネントについて考えます.
投稿情報は,以下の内容によって構成されるとします.

  • アバター画像とユーザ名から成る投稿者情報(UserInfo)
  • テキストと投稿日時から成ると投稿内容情報(ContentInfo)

これを実現するコンポーネントの組み合わせ例を以下に示します.

// 投稿者情報コンポーネント
const UserInfo = class extends React.Component {
    render() {
        return (
            <div id="user-info">
                <img
                    src={this.props.userInfo.imgUrl}
                    alt={this.props.userInfo.userName}
                />
                <h2>{this.props.userInfo.userName}</h2>
            </div>
        );
    }
}

// 投稿内容情報コンポーネント
const ContentInfo = class extends React.Component {
    render() {
        return (
            <div id="content-info">
                <p>{this.props.contentInfo.text}</p>
                <p>{this.props.contentInfo.date}</p>
            </div>
        )
    }
}

// 投稿情報コンポーネント
const PostData = class extends React.Component {
    render() {
        return (
            <div id="post-data">
                <UserInfo userInfo={this.props.postData.userInfo} />
                <ContentInfo contentInfo={this.props.postData.contentInfo} />
            </div>
        )
    }
}

// propsに渡すデータ
const postData = {
    userInfo: {
        imgUrl: "example.com/hoge.jpg",
        userName: "Szarny"
    },
    contentInfo: {
        text: "hello from component",
        date: new Date().toLocaleTimeString()
    }
}

// レンダリング
ReactDOM.render(
    <PostData postData={postData} />,
    document.getElementById("root")
)

State

Reactコンポーネントは,自身の状態を保持するためにpropsとは異なるstateというプロパティを持ちます.
stateのもっとも単純な利用方法は以下の通りです.

class ... {
    constructor(props) {
        super(props);
        this.state = { // 監視したい値 }
    }

    componentDidMount() {
        // DOMがマウントされた際に実行する処理
    }

    componentWillUnmount() {
        // DOMがアンマウントされた際に実行される処理
    }

    somefunction(){
        this.setState({
            // 変更したい値
        });
    }

    render(){
        ....
    }
}

以下は,stateを利用したカウンターの実装例です.

const Counter = class extends React.Component {
    constructor(props) {
        super(props);
        this.state = { value: 0 };
    }

    componentDidMount() {
        this.timerId = setInterval(
            () => this.increment(),
            1000
        );
    }

    componentWillUnmount() {
        clearInterval(this.timerId);
    }

    increment() {
        // prevState(1つ前のstate)のvalueと
        // propsで与えられたincrementを加算して
        // this.state.valueにセット
        this.setState((prevState, props) => ({
            value: prevState.value + props.increment
        }))
    }

    render() {
        return (
            <p>Counter: {this.state.value}</p>
        );
    }
}

let increment = 1;

ReactDOM.render(
    <Counter increment={increment} />,
    document.getElementById("root")
);

おわりに

またの機会に,ドキュメントの後半をやっていきます☺