ラベル React の投稿を表示しています。 すべての投稿を表示
ラベル React の投稿を表示しています。 すべての投稿を表示

2017年5月28日日曜日

React + Redux 非同期処理を導入

前回に引き続き, React + Redux です。 今回は前回のサンプルにajax 非同期処理を導入します。 簡単な例として実装するため今回は, ページロードの際にjsonデータを取ってきてそれをリストで描画する 例にしました。 前回のエントリーはこちら React + Redux React Router その前 React + Redux とりあえず簡単な例から サンプルは, Reddit(Redux公式より) を参考にしています。 このサンプルとの違いはデータ取得をイベントフックにしているかロード時に取得しているかの違いです。

データの準備

データはjsonファイルとして準備します。前回のプロジェクトのルートにフォルダを作成し, データを置いておきます。 このデータをwebpack-dev-serverがなんとかしてくれます。
first (プロジェクト名)
|- data
     |- data.json

data.json
{
  "data" : [
    {
      "index" : 1,
      "name" : "Taro"
    },
    {
      "index" : 2,
      "name" : "Jiro"
    }
  ]
}
これで, localhost:8080/data/data.json でデータを取得できるようになりました。

HttpClientはどうする?

React そのものには, HttpClientは含まれていません。ですので何かしらのライブラリの導入をするか, pure Javascript で頑張るかのどちらかになります。 選択肢としてはいくつかあります。 せっかく, Reactを使っているのに jQueryとか, Angularはないよな。と思いつつ。 SuperAgent, Axios などは割と使われているみたいです。 今回は, Fetchを使ってみます。
npm insatll whatwg-fetch --save

React-ThunkとMiddleware

今回のサンプルを実装するに当たって重要なのが React-Thunkと Middlewareです。 Redux Thunk (Thunk middleware for Redux.) これを使うことでAction CreatorがAction objectを返す代わりにfunctionを返すことができます。
npm install redux-thunk --save

非同期データとアクション

ここで非同期でデータを取ってくるところとアクションの関係を説明します。 データを取ってくる = データを取りに行く + データを受信した or データ取得に失敗した という形に分離できます。これでアクションは3種類? 実装する必要がありますね。 (結局このあたりの細かいところをどう設計するかが鍵になるのかと思います。最初のうちはさっぱりわからないですよね。)

サンプル

構成は前回のエントリーの続きになります。 webpackの設定などの変更はしていないです。
first
|-data
|  |- data.json
|-.babelrc
|-index.html
|-package.json
|-webpack.config.js
|-src
   |-actions
   |    |- index.jsx
   |-components
   |    |- FormDisplay.jsx
   |    |- FormInput.jsx
   |-containers
   |    |- AppContainer.jsx
   |    |- FormApp.jsx
   |    |- Page2.jsx
   |    |- Page2Container.jsx
   |-reducers
        |- fetchReducer.jsx
        |- formreducer.jsx
        |- index.jsx

index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { reducers } from './reducers/index.jsx';
import thunkMiddleware from 'redux-thunk';

import AppContainer from './containers/AppContainer.jsx';
import Page2Container from './containers/Page2Container.jsx';

const initialState = {
    formReducer: {
        value : null
    },
    fetchReducer: {
        items: []
    }
};

const store = createStore(reducers,
    initialState,
    applyMiddleware(thunkMiddleware)); // reducer, initial state

// Rendering
ReactDOM.render(
  
      
          
            
            
          
      
  ,
    document.querySelector('.content')
);
変更したのは, redux-thunk をmiddlewareの設定にしたことこれで ActionCreatorでfunctionが返せます。

reducers/fetchReducer.jsx
export function fetchReducer(state = {}, action) {
    switch(action.type) {
        case 'REQUEST_DATA_SUCCESS':
            return Object.assign({}, state, {
                items: action.data,
                lastUpdated: action.receivedAt
            });
        case 'REQUEST_DATA_FAIL': {
            return Object.assign({}, state, {
                items: action.data
            });
        }
        default:
            return state;
    }
}
items というデータのArrayを返します。これはデータのリクエストが成功した時(その時はデータ) 、失敗した時に返します(その時は空[])

reducers/index.jsx
import { combineReducers } from 'redux';
import { formReducer } from './formreducer.jsx';
import { fetchReducer} from './fetchReducer.jsx';

export const reducers = combineReducers({
    formReducer,
    fetchReducer
});
前回作成した、reducerと共に利用するので2つのReducerをcombineReducersで 繋げます。

actions/index.jsx
const SEND = 'SEND';

/* Action Creator */
// Return Object
export function send(value) {
    // Action
    return {
        type: SEND,
        value,
    };
}

/* Action2 */

const REQUEST_DATA = 'REQUEST_DATA';
const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS';
const REQUEST_DATA_FAIL = 'REQUEST_DATA_FAIL';

function invalidateData() {
    return {
        type: REQUEST_DATA_FAIL,
        data: []
    }
}

function requestData() {
    return {
        type: REQUEST_DATA
    }
}

function receiveData(json) {
    return {
        type: REQUEST_DATA_SUCCESS,
        data : json,
        receivedAt: Date.now()
    }
}

export function fetchData() {
    return function (dispatch) {
        dispatch(requestData());
        return fetch('/data/data.json')
            .then(response => response.json())
            .then(json => dispatch(receiveData(json.data)))
            .catch(e =>
                dispatch(invalidateData()));
    }
}
前回作成したActionCreatorに3つのActionを追加しました。 データ取得できなかった場合のAction, リクエスト開始時のAction, データが取得できた場合のActionです。これらのアクションをまとめた fetchデータというfunctionをexportしています。

containers/Page2.jsx
import React from 'react';
import PropTypes from 'prop-types';
import {fetchData} from '../actions/index.jsx';

class Page2 extends React.Component {

    constructor(props) {
        super(props);
    }

    componentDidMount() {
       const { dispatch } = this.props;
    //    console.log(dispatch);
       dispatch(fetchData());
    }

    render() {
        const { items } = this.props;

        var list = [];
        for(var index in items){
            list.push(
  • {items[index].name}
  • ); } return (
      {list}
    ); } } Page2.propTypes = { items: PropTypes.array }; export { Page2 as default };
    componentDidMountの所でデータを非同期に取得しています。renderの部分データを取り出し リストを作成して描画します。
    最後は Containerですね。 containers/Page2Container.jsx
    // Connect to Redux
    function mapStateToProps(state) {
        return {
            items: state.fetchReducer.items
        }
    }
    
    const Page2Container = connect(
        mapStateToProps
    )(Page2)
    
    export { Page2Container as default };
    
    この部分でPropertyとComponentを繋ぎます。

    テスト

    動作確認
    webpack-dev-server
    
    localhost:8080/member にアクセス。2つのアイテムのあるリストが見えるはずです。

    React + Redux React Router

    前回に引き続き, React + Reduxを見ていきます。 今回は 前回のサンプルにReact Routerを導入します。 前回のサンプルはこちらをご覧ください。 (React + Redux とりあえず簡単な例から)

    React Routerを導入

    npm install --save react-router-dom
    
    Versionは, 4.1.1 を使用。このバージョンが曲者っぽい。 4以下の書き方などは違う模様。 また, react-router というパッケージと react-router-dom は違うみたい。 前はreact-routerを使っていた気がするんだけどなぁ。 今回は, react-router-domを使います。

    Container の作成

    今回はもう1ページ用にもう一つContainerを作成します。Reduxの流れを作るのが面倒なので 簡単なComponentにしておきます。
    containers/Page2.jsx
    class Page2 extends React.Component {
       render() {
            return (
                
    Hello!
    ); } } export { Page2 as default };
    ただのSimpleなdivです。

    React-Routerを入れる

    前回作った部分を/appとして今回の部分を/memberと二つにします。 (コードは一部のみ)
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
    
    import AppContainer from './containers/AppContainer.jsx';
    import Page2 from './containers/Page2.jsx';
    
    
    ReactDOM.render(
      
          
              
                
                
              
          
      ,
        document.querySelector('.content')
    );
    
    前回と違うところは Prividerのしたのところに, RouterというタグとSwitchというタグ, その中に各ページの定義 Routeが入っています。 これで localhost:8080/app, localhost:8080/member で各ページが確認できます。

    React + Redux とりあえず簡単な例から

    久々の投稿。ここしばらくは, モバイル, フロントがメインでたまにバックエンドの開発をする日が続いています。
    仕事では, Angular 1 がメインでしたそろそろReact + webpackでいこうと模索しています。
    React をしばらく触って, それからReact + Redux の学習を初めています。
    React は本読んだりしてなんとかしましたが, redux にはかなり苦労しています。


    参考にしたもの

    一番上の解説はわかりやすかったです。今回のサンプルもここで取り上げていたサンプルをベースに作っています。 下2つはReduxの公式のサンプルです。フォルダの構成とか結構参考になると思います。

    最終的なゴール

    • (今回)webpackでビルドする
    • (今回)webpack-dev-server で動作確認
    • (今回)reduxの導入
    • (今回)es6でかけるようにする
    • (今回)action, container, component, reducerの構成を作る
    • react-routerを使う
    • ajaxでデータを取ってくる
    react-routerと, ajaxに関しては次回以降取り上げたいと思います。

    バージョンなど

    作成日 : 2017/05/28 webpack : 2.5.1
    プロジェクトの作成, react, webapack, redux の導入
    mkdir first     
    cd first
    npm init -y
    npm install --save webpack webpack-dev-server
    npm install --save-dev babel babel-core babel-loader babel-preset-es2015 babel-preset-react
    npm install --save react react-dom react-redux redux
    npm install --save prop-types
    
    firstはプロジェクト名です。なんでも良いです。 今回は, webpack, webpack-dev-server, react, react-redux, reactのプロパティを使うとWarningが出るので prop-typesを使います。

    フォルダ構成
    first
    |- dist
    |- src
    |   |- actions
    |   |- components
    |   |- containers
    |   |- reducers
    |   |- index.jsx
    |- .babelrc
    |- index.html
    |- package.json
    |- wbpack.config.js
    


    .babelrc

    ここで, es6を使うための設定をします。
    {
      "presets": [
        "es2015", "react"
      ]
    }
    


    webpack.config.js

    webpack の設定です。今回は jsx を動作させるための設定だけを記載しました。
    const path = require('path');
    
    module.exports = {
        entry: path.join(__dirname, 'src/index.jsx'),
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'bundle.js'
        },
        devServer: {
            contentBase: '.',
            port: 8080,
            inline: true,
            historyApiFallback: true
        },
        module: {
            loaders: [
                {
                    test: /\.jsx$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader'
                }
            ]
        }
    };
    
    webpackでビルドした結果を distフォルダへ, babel-loaderを使って, jsxを処理します。 webpack-dev-server はポート8080で動作させます。

    Redux

    Redux に関しては他のブログなどのエントリーを参考にしてさらっとまとめます。 Redux : Redux 細かい話は公式などを確認した方が良さそう。 状態管理をアプリ全体で行う, 状態管理するStoreはアプリ全体で1つ。 ユーザのActionがコールされると, Recuderを使って, Storeの状態を更新する。 ここで登場人物が3つ出てきました。(Action, Reducer, Store) Reduxではデータフローが重要で, データフローのルールに従います。 以下各要素の説明とプログラム上での登場人物の整理です。

    Action

    typeをキーとしてStateを更新するための定義を書く。 状態更新はdispatch関数を実行して行う (ajaxの通信はここで行う) ActionはAction名(Reducerで処理を判別するため)と状態の値を持った単なるオブジェクト Storeのdispatch() メソッドの引数にAction Creatorを渡すことでActionがReducerに送られる

    ActionCreator

    最終的には, Actionと同じファイルに書いている functionで Action = object を返す Actionで書いていない更新したい状態の定義を書く。 Stateの更新自体は, Reducerがするので更新したい部分を引数などから生成する

    Reducer

    Reducerは、現在の状態(state)と受け取ったActionを引数に取り、新しい状態を返す関数 typeをキーにして処理を分ける, Switch 文中に Action で定義した State の type で分岐しロジックを書く stateの更新には, ES2015 Object.assign()を使おう!! Object.assign() はmergeオブジェクトのマージを行います。 ※Reducerは複数作ることができます。(ページごとに作るイメージかなぁ) (storeは一つしか引数に取れないので combineReducersで combine)

    Store

    状態管理のオブジェクト 初期データと, ReducerからcreateStoreメソッドで作成します。これをProvider渡します。 Reduxのmiddlewareを渡す場合はそれも引数に加えます。

    Container Components

    親コンポーネント。ここでReactとReduxのつなぎ込みを行います。 Container ComponentsはPresentational Componentを持ちます。

    Presentational Component

    子コンポーネント。最小単位のコンポーネント 親からプロパティを受け取り描画します。イベントなども親から受け取ります。

    サンプル

    今回のサンプルは参考の一番上のエントリーを参考にreact + reduxをwebpackで動作させる, フォルダ構成をReactの公式サンプルを 元に作り変えたものです。 index.jsx エントリー部分。
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    import { createStore } from 'redux';
    import { Provider } from 'react-redux';
    import { reducers } from './reducers/index.jsx';
    
    import AppContainer from './containers/AppContainer.jsx';
    
    const initialState = {
        formReducer: {
            value : null
        }
    };
    
    const store = createStore(reducers,
        initialState); // reducer, initial state
    
    // Rendering
    ReactDOM.render(
      
          
      ,
        document.querySelector('.content')
    );
    


    とりあえず必要なComponentなどを全部importしています。 ここに直接書くのは, 初期状態の設定と(この部分もファイル分離できる) store の作成と, レンダリングの部分ですね。 Reactに連携させるReduxのStoreを渡しています。Providerという部分でReactとReduxのStoreの連携をしていますね。

    index.html 元になるView,ただし動作関係は全部上のindex.jsxに任せる
    
    
    
        
        Redux Sample
    
    
    
    ビルドした後のjsを取り込んでいるだけです。この部分はもう基本的には変更しません。

    actions/index.jsx アクションの実装です。Actionと,ActionCreatorの実装をしています。
    const SEND = 'SEND';
    
    /* Action Creator */
    // Return Object
    export function send(value) {
        // Action
        return {
            type: SEND,
            value,
        };
    }
    
    Actionは, objectを返します。 ここでは, Actionタイプ(アクションの名前みたいなもの)とアクションを発行した部分から valueという値を受け取ってそれを返しています。

    recucers/formreducer.jsx Reducerの実装です。
    export function formReducer(state = {}, action) {
        switch(action.type) {
            case 'SEND':
                return Object.assign({}, state, {
                    value: action.value,
                });
            default:
                return state;
        }
    }
    


    containers/FormApp.jsx
    import React from 'react';
    import PropTypes from 'prop-types';
    
    import FormInput from '../components/FormInput.jsx';
    import FormDisplay from '../components/FormDisplay.jsx';
    
    class FormApp extends React.Component {
        render() {
            return (
                
    ); } } FormApp.propTypes = { onClick: PropTypes.func.isRequired, value: PropTypes.string }; export { FormApp as default };


    containers/AppContainer.jsx
    import { connect } from 'react-redux';
    import { send } from '../actions/index.jsx';
    import FormApp from './FormApp.jsx';
    
    // Connect to Redux
    function mapStateToProps(state) {
        return {
            //value: state.value
            value: state.formReducer.value    //http://qiita.com/usagi-f/items/ae568fb64c2eac882d05
        }
    }
    
    function mapDispatchToProps(dispatch) {
        return {
            onClick(value) {
                dispatch(send(value));
            }
        };
    }
    
    const AppContainer = connect(
        mapStateToProps,
        mapDispatchToProps
    )(FormApp)
    
    export { AppContainer as default };
    
    ここで Redux のプロパティとComponentを繋ぐ作業をしています。 storeからデータをひっぱてくるところ, メソッドを渡すとこですね。

    components/FormDisplay.jsx
    class FormDisplay extends React.Component {
        render() {
            return (
                
    {this.props.data}
    ); } }; FormDisplay.propTypes = { data: PropTypes.string }; export { FormDisplay as default };


    components/FormInput.jsx
    class FormInput extends React.Component {
        send(e) {
            e.preventDefault();
            this.props.handleClick(this.myInput.value.trim());
            this.myInput.value = '';
            return;
        }
        render() {
            return(
                
    (this.myInput = ref)} defaultValue="" />
    ); } }; FormInput.propTypes = { handleClick: PropTypes.func.isRequired }; export { FormInput as default };


    動作確認

    webpackでビルド, webpack-dev-serverで簡易サーバーを動かします。 ビルド
    webpack
    
    サーバーを動かす
    webpack-dev-server
    
    localhost:8080/index.html へアクセス