太陽がまぶしかったから

C'etait a cause du soleil.

React 入門〜React Component の表示と Restful API コール

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

はじめての React

 上記で構築した Docker 環境で Laravel x React に入門してみる。 基本的には resources/views/welcome.blade.php から紐づけた resources/assets/js/components/Example.js を編集していく。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
    render() {
        return (
            <div>
                <h1>会社管理</h1>
                <CompanyInput />
            </div>
        );
    }
}

class CompanyInput extends Component {
    render() {
        return (
            <div><input placeholder="会社名" /> <button>登録</button></div>
        );
    }
}

if (document.getElementById('example')) {
    ReactDOM.render(<App />, document.getElementById('example'));
}

f:id:bulldra:20180505204141j:plain

 基本的には Component を継承した class の render() で JSX をreturn する形でコンポーネントを作成し。作成したコンポーネントは別のコンポーネントの JSX 内から利用できるので、親子関係で最終的な表示定義を作成して ReactDOM で現実のDOMに紐付けると理解。

React Component の引数

 React Component への引数はタグの属性または子要素で定義する。タグの属性に {} で引数を指定するとコンポーネントクラスの this.props 配下に格納される。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
    render() {
        
        return (
            <div>
                <h1>会社管理</h1>
                <CompanyInput placeholder={ "会社名を入れてね" }/>
            </div>
        );
    }
}

class CompanyInput extends Component {
    render() {
        return (
            <div><input placeholder={ this.props.placeholder } /> <button>登録</button></div>
        );
    }
}


if (document.getElementById('example')) {
    ReactDOM.render(<App />, document.getElementById('example'));
}

f:id:bulldra:20180505205337j:plain

 JSXのタグ全体の文字列はもちろん、属性値も " で囲わない記法に違和感を感じつつも、下手にクォートさせてもバグの温床になるだけという割り切りが React の特徴らしいので慣れの問題か。

React Component 内での繰り返し記法

 React Component 内で繰り返しを行うには Array.prototype.map() 関数の理解が必要になる。 map 関数とは配列の要素ごとにコールバック関数を実行する関数。Component の引数として受け取った配列の要素ごとに JSX を返却する関数を定義することで繰り返しを実現する。

class App extends Component {
    render() {
        const companies = [
            {company_name:'株式会社React', company_id: 1 },
            {company_name:'株式会社Vue', company_id: 2 },
            {company_name:'Angular株式会社', company_id: 3 },
        ];        
        return (
            <div>
                <h1>会社管理</h1>
                <CompanyInput placeholder={ "会社名を入れてね" }/>
                <CompanyList companies={ companies } />
            </div>
        );
    }
}

class CompanyInput extends Component {
    render() {
        return (
            <div><input placeholder={ this.props.placeholder } /> <button>登録</button></div>
        );
    }
}

class CompanyList extends Component {
    render() {
        const companiesMap = this.props.companies.map(c => {
            return <li key={c.company_id}>{ c.company_name }</li>; 
        });
    
        return (
            <ol>{ companiesMap }</ol>
        );
    }
}

f:id:bulldra:20180505211207j:plain

 companies の要素を <li> タグに変換する map 関数の定義を companiesMap に格納して、 JSX 内で展開している。DOMに対して繰り返しで子要素を追加する場合には key 属性が必要となるため company_id を割りあてている。

状態を持つ必要のないコンポーネントSFC

 会社情報をテーブルで表示する場合は会社要素自体をコンポーネント化したいが、都度でコンポーネントクラスを作っていくと、とり得る状態の組み合わせが複雑化していくし、状態変更に応じた処理が重くなっていくため、画面上での状態を持つ必要がないコンポーネントについては、Stateless Functional Components 化すべきとのこと。

class CompanyList extends Component {
    render() {
        const companiesMap = this.props.companies.map(c => {
            return <CompanyItem key={c.company_id}  { ...c } />; 
        });
    
        return (
            <table>
                <thead>
                    <tr><th>会社ID</th><th>会社名</th></tr>
                </thead>
                <tbody>
                    { companiesMap }
                </tbody>
            </table>
        );
    }
}

function CompanyItem (props) {
    return (
        <tr>
            <td>{ props.company_id }</td>
            <td>{ props.company_name }</td>
        </tr>
    );
}

 上記の CompanyItem() のように props を関数の引数として受け取って、そのまま JSX を返却するだけ。 props を変更しても影響ないが、やっても仕方がないだろう。

f:id:bulldra:20180505213449j:plain

  return <CompanyItem key={c.company_id} { ...c } />; の { ...c } はオブジェクトの要素を展開するスプレッド演算子return <CompanyItem key={c.company_id} company_name={c.company_name} company_id={c.company_id} />; と同じ意味となる。

 とりあえず、静的な JSX の表示処理はなんとなく理解できたので、コンポーネント状態の定義と状態に応じた表示処理について実装していく。

fetch による API コールと動的UI反映

 上記で作成した React Component を拡張して RESTful API をコールできるようにしたい。非同期のAPIコールを実現するために fetch() を利用することにした。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class App extends Component {
    constructor (props) {
        super(props);
        this.state = { companies: [] };
        this.reloadCompanyApi = this.reloadCompanyApi.bind(this);
    }

    reloadCompanyApi() {
        fetch('/api/company', {
            method: 'GET',
            headers: { 'content-type': 'application/json' }, 
        }).then(res => {
            if(res.ok) {
                return res.json();
            } else {
                console.log('error!');
            }
        }).then(json => {
            const companies = json.map(r => { 
                return { 
                    company_id: r.company_id, 
                    company_name: r.company_name 
                };
            });
            companies.sort(function (a, b) { return -(a.company_id - b.company_id); });
            this.setState({ companies: companies });         
        });
    }

    componentWillMount() {
        this.reloadCompanyApi();
        setInterval(this.reloadCompanyApi, '5000');
    }

    render() {
        return (
            <div>
                <h1>会社管理</h1>
                <CompanyList companies={ this.state.companies } />
            </div>
        );
    }
}

class CompanyList extends Component {
    render() {
        const companiesMap = this.props.companies.map(c => {
            return <CompanyItem { ...c } key={ c.company_id } />; 
        });
    
        return (
            <table>
                <thead>
                    <tr><th>会社ID</th><th>会社名</th></tr>
                </thead>
                <tbody>{ companiesMap }</tbody>
            </table>
        );
    }
}

function CompanyItem (props) {
    return (
        <tr>
            <td>{ props.company_id }</td>
            <td>{ props.company_name }</td>
        </tr>
    );
}

if (document.getElementById('app')) {
    ReactDOM.render(<App />, document.getElementById('app'));
}

f:id:bulldra:20180506222425p:plain

 fetch() の利用法については『Fetch API - Web API | MDN』を参照。開始時および 5秒に1回 API から取得した JSON を展開してコンポーネントの state に設定。 React では this.setState() が実行されるたびに、 render() が再起動されるため、DBを更新すればページリロードをせずに動的にUI反映が行われる。

Post によるデータ登録とUI反映

 続いて POST メソッドを呼び出して登録する処理を App クラスに作成する。

class App extends Component {
    constructor (props) {
        super(props);
        this.state = { companies: [] };
        this.reloadCompanyApi = this.reloadCompanyApi.bind(this);
        this.createCompanyApi = this.createCompanyApi.bind(this);
    }

    createCompanyApi(company_name) {
        fetch('/api/company', { 
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ company_name: company_name }),
        }).then(res => {
            if(res.ok) {
                return res.json();
            } else {
                console.log('error!');
            }
        }).then(json => {
            this.reloadCompanyApi();
        });
    }

 このメソッドは入力用のコンポーネントから呼び出すため、コンストラクタで bind() を行う。この処理をおこなっておくと、子コンポーネントの属性として関数を受け渡すことができる。

    render() {
        return (
            <div>
                <h1>会社管理</h1>
                <CompanyInput 
                    placeholder={ "会社名" }
                    createCompany={ this.createCompanyApi }
                />
                <CompanyList companies={ this.state.companies } />
            </div>
        );
    }

 CompanyInput コンポーネントではテキストボックスの中身を state に変換して、ボタン押下をトリガに受け渡された createCompany を起動する。

class CompanyInput extends Component {
    constructor (props) {
        super(props);
        this.state = { inputValue:'' };
        this.handleChange = this.handleChange.bind(this);
        this.handleCreateClick = this.handleCreateClick.bind(this);
    }

    handleChange(e) { this.setState({ inputValue: e.target.value }); }
    handleCreateClick() { 
        this.props.createCompany(this.state.inputValue);
        this.setState( { inputValue:'' } );        
    }
    render() {
        return (
            <div>
                <input
                    placeholder={ this.props.placeholder }
                    value={ this.state.inputValue }
                    onChange={ this.handleChange }
                />
                <button onClick={ this.handleCreateClick }>登録</button>
            </div>
        );
    }
}

f:id:bulldra:20180506224931p:plain

 登録処理のレスポンスが戻り次第UIにも動的に反映される。試しに2つのタブを開いて追加した場合でも、もう一方のタブに自動反映される。課題は山積みだけど、Laravel(PHPを含めて) や React を5月から触りはじめて何も分からない状態からなんとなく進捗してきたので、ブラッシュアップしたい。