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つのアイテムのあるリストが見えるはずです。

    0 件のコメント:

    コメントを投稿