React は Viewしか担当していないので、その他の機能を含めたアプリのスケルトン作成を目指す。

Node.js、npm パッケージ管理、および、ブラウザが対応していないECMAScriptバージョンの利用にBabelを利用するなどを含めた環境構築、ページ遷移のためのReact Router、アプリケーションアーキテクチャとしてのRedux、非同期処理のためのRedux-Saga をとりあえず、最低限のレベルで組み合わせた。

  1. React 開発の全体像を把握しつつ開発環境を整える
  2. React の単純なサンプルに React Router を適用する
  3. React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用する

次は、Ajax通信部分を組み込む。

jQuery.ajaxの代わりにSuperAgentを使う

Ajax の部分だけを利用するのに、jQueryはオーバースペック(かつ容量を無駄に消費)してしまうため、HTTP通信の特化した、SuperAgetnt を使用することとする。

github : https://github.com/pppiroto/react_get_start.git

1.インストール

SuperAgent

npm install --save superagent

2.実装

2.1 ダミーAPIの結果

まずは、疎通をしたいだけなので、APIの結果を想定した、json ファイルを作成する。

/hello_successurl.json

{
    "message":"HELLO FROM API"
}

2.2 Action

以下のActionを追加する。

  • HELLO_API : 上記で作成したダミーjsonを取得する場合に呼び出す
  • HELLO_API_SUCCESS : 取得が成功した場合に呼び出す
  • HELLO_API_FAIL : 取得が失敗した場合に呼び出す

/actions/index.js

import { createAction } from 'redux-actions';

export const HELLO = 'HELLO';
export const HELLO_API = 'HELLO_API';
export const HELLO_API_SUCCESS = 'HELLO_API_SUCCESS';
export const HELLO_API_FAIL = 'HELLO_API_FAIL';

export const helloAction = createAction(HELLO);
export const helloApiAction = createAction(HELLO_API);
export const helloApiSuccessAction = createAction(HELLO_API_SUCCESS);
export const helloApiFailAction = createAction(HELLO_API_FAIL);

2.3 Ajax API

  • Ajax 通信を行い、データを取得する関数を作成、ここで、SuperAgentを使用する。
  • データの非同期取得のために、Promise を使用する(Promiseについては、こちらを参照)

以下は確認用のやっつけコード

  • id として、2.1 のダミーファイル名の一部を渡すことで、取得成功、失敗を確認
  • クエリパラメータとしてダミーのランダム文字列を付与することで、ブラウザがキャッシュするのを抑制

services/helloApi.js

import request from 'superagent';

// http://qiita.com/salesone/items/356572e689b9c2099c5c
export function helloApi(id) {
    console.log("helloApi(" + id + ")");
    return new Promise(
        (resolve, reject) => {
            request
                .get("./hello_{0}.json?dt=".replace("{0}", id) + Math.random().toString().replace(".", ""))
                .end(function (err, res) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve({ payload: res.body });
                    }
                });
        }
    );
}

2.4 Sagas

上記で作成したAPIを呼び出し、成功/失敗に応じたActionを値をセットし呼び出す。
/sagas/index.js

import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import {
    HELLO, helloAction,
    HELLO_API, helloApiAction,
    HELLO_API_SUCCESS, helloApiSuccessAction,
    HELLO_API_FAIL, helloApiFailAction
} from '../actions';
import { helloApi } from '../services/helloApi';

export function* getHelloApi(action) {
    console.log(helloApiSuccessAction());
    try {
        console.log(action);
        const data = yield call(helloApi, action.payload.id);
        yield put(helloApiSuccessAction(data.payload));
    } catch (error) {
        yield put(helloApiFailAction(error));
    }
}

// https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
export default function* rootSaga() {
    //yield* takeEvery(HELLO, helloAction);
    yield* takeEvery(HELLO_API, getHelloApi);
}

2.5 Reducer

Actionに設定された値から状態値を生成する。

ダミーの単純な処理のため、識別のため、末尾に処理時間を付与している。

/reducers/hello.js

import { HELLO, HELLO_API, HELLO_API_SUCCESS, HELLO_API_FAIL } from '../actions';

export default function hello(state="", action) {
    var suffix = " " + (new Date()).toLocaleTimeString("ja-JP");
    switch (action.type) {
        case HELLO:
            return action.payload + ",hello" + suffix;
        case HELLO_API_SUCCESS:
            return action.payload.message + suffix;
        case HELLO_API_FAIL:
            return action.payload.toString() + suffix;
        default:
            return state;
    }
}

2.6 App

  • Home コンポーネントに、成功/失敗のAPI呼び出しボタンを配置。
  • name 属性の値をActionに引き渡す

/containers/app.js

import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'
import { helloAction, helloApiAction } from '../actions';

class Home extends Component {
  handleMessage() {
    this.props.dispatch(helloAction('Yes'));
  }
  handleMessageApi(event) {
    this.props.dispatch(helloApiAction({id:event.target.name}));
  }
  render () {
    return (
      <div>
        <h2>Home</h2>
        { this.props.hello }
        <br />
        <button onClick={ this.handleMessage.bind(this) } >Hello</button>
        <br />
        <button name="successurl" onClick={ this.handleMessageApi.bind(this) } >API(Sucess)</button>
        <br />
        <button name="failurl" onClick={ this.handleMessageApi.bind(this) } >API(Fail)</button>
      </div>
    );
  }
}
 
const About = () => (
  <div><h2>About</h2></div>
)
const Topics = () => (
  <div><h2>Topics</h2></div>
)

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
          </ul>
          <hr />
          {/* http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a */}
          <Route exact path="/" component={connect(state => state)(Home)} />
          <Route exact path="/about" component={About} />
          <Route exact path="/topics" component={Topics} />
        </div>
      </BrowserRouter>
    );
  }
}

// http://qiita.com/MegaBlackLabel/items/df868e734d199071b883#_reference-863a1e1485bf47f046e5
function mapStateToProps(state) {
  return {
    message:state.hello
  };
}

// https://stackoverflow.com/questions/43350683/react-router-uncaught-typeerror-cannot-read-property-route-of-undefined
// export default withRouter(connect(mapStateToProps)(App))
export default connect(state => state)(App)

3.実行

3.1 成功

成功ボタンを押下、json取得し、Actionによりstateが更新され、そうていされた結果が表示されたOK。

super_agent01

3.2 失敗(存在しないURLを指定)

失敗ボタン押下で、存在しないURLへアクセスしに行き、結果エラーメッセージが画面表示されたOK。

super_agent03

ようやく最低ラインにたどり着いた。。。

フロントエンドにReactを使ったアプリを作成したいが、React は Viewしか担当していないこともあり、なかなか敷居が高い。ステップをおって、アプリケーションのスケルトンを作成することを目的とする。

React 開発の全体像を把握しつつ開発環境を整える

で、Reactの開発環境の構築をおこない、

React の単純なサンプルに React Router を適用する

で、React Router を導入して、ページ遷移を適用した。

今回、Redux および、Redux-Saga を適用して、データの取り回し、および非同期タスクの取り扱いの枠組みを導入する。

以下、公式リファレンス

参考

概念的にも、非常にややこしい。以下のサイトのサンプルを参照、内容をメモしながら進める。

1.Redux

1.1 アーキテクチャ

  • FacebookのFluxアーキテクチャに基づいて、設計された
  • Flux アーキテクチャ : Action –> Dispatcher –> Store –> View(React) の4部品からなり、データの流れは1方向

flux-diagram-white-background

  • Redux アーキテクチャ: Action –> Reducers –> Store の3部品からなり、Flux同様データは一方向に流れる

  • Store(アプリケーションの状態管理) は、アプリケーションに1つのみ

  • Actionが発生した際、Reducerを使用して、Storeの状態(State)を変更する

  • Storeの用意するAPIを利用することで、Viewは、state更新の購読とstateの取得を行う

1.2 3原則

  1. Single source of truth (信頼できる唯一の情報源) : アプリケーション全体のStateは、一つのStoreに格納
  2. State is read-only (state は 読み取り専用) : State 変更は、必ず Action 経由
  3. Changes are made with pure functions (変更は純粋関数によってなされる) : 副作用のないReducer でActionによる、Stateの変更方法を指定。

1.3 実装

(1) Provider、connect関数、Containerコンポーネント

Reduxでコンポーネントを再利用する が、概念と実装の対応について分かりやすく書かれていて非常に参考になる。

  • Providerコンポーネントは唯一Storeを持つことを許された存在
  • connect 関数でReactコンポーネントをラッピングしたものがContainerコンポーネントになります(Connected Component)
  • Contextを使ってStateやdispatch関数を配下のContainerコンポーネントで利用可能にします

(2) connect の実装パターンについて

ReactとReduxを結ぶパッケージ「react-redux」についてconnectの実装パターンを試す  非常に参考になった。

2.Redux-Saga

redux-sagaで非同期処理と戦う を参考に。

2.1 Redux-Saga とは

  • 別々に並行処理される「タスク」実行環境を、React-Redux に提供する
  • Redux-Sage API
    • select : stateから必要なデータを取り出す
    • put : Action を dispatch する
    • take : Action を待つ
    • call : Promise の完了を待つ
    • fork : 別のタスクを開始する
    • join : 別のタスクの完了を待つ
  • 非同期処理を同期的に記述できるようにしつつ、複数のタスクを同時並行に実行でき
  • る。

    上記サイトより引用
    この図は、永久保存版じゃなかろうか。よくわかる。

redux-saga-system

りだっくすさが(redux-saga)に入門する

3.サンプル

という概要を踏まえて、

  1. React開発環境を整え
  2. React Router を組み込んだ

状態 に以下の変更を施し、Redux、Redux-Saga 適用スケルトンを目指す。

https://github.com/pppiroto/react_get_start.git

3.1  app.js の移動

上記、1.3(1) で引用したように、connect関数でReactコンポーネントをラップすることで、Containerコンポーネントとなる。Appコンポーネントを、connect関数にラップする。これに沿って、プロジェクトの階層を変更する。

  • /app.js –> /conatiners/app.js に移動
  • /entry.js を新規作成
  • webpack.config.js の起点を変更する
    • entry: './src/app.js'  -> entry: './src/entry.js'

3.2 /entry.js

import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/app';
import configureStore from './store/configureStore';
import { Provider } from 'react-redux';

const store = configureStore();

ReactDOM.render(
    // http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a
    // Providerコンポーネントは唯一Storeを持つことを許された存在
    // Contextを使ってStateやdispatch関数を配下のContainerコンポーネントで利用可能にします
    // Reduxにおいては connect 関数でReactコンポーネントをラッピングしたものがContainerコンポーネントになります(Connected Component )
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

3.3 /containers/app.js

  • ソースの末尾で、Appコンポーネントをconnect関数でラップしている。これを上記entry.jsで使用している。
  • Home コンポーネントで、stateの使用とActionの発行ができるか確認するために書き換える
  • Routeの compoonent 指定時に、Homeコンポーネントを connect関数を用いて、Containerコンポーネント化
  • この図をイメージredux-saga-container
import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'
import { helloAction } from '../actions';

class Home extends Component {
  handleMessage() {
    this.props.dispatch(helloAction());
  }
  render () {
    return (
      <div>
        <h2>Home</h2>
        { this.props.hello }
        <br />
        <button onClick={ this.handleMessage.bind(this) } >Hello</button>
      </div>
    );
  }
}
 
const About = () => (
  <div><h2>About</h2></div>
)
const Topics = () => (
  <div><h2>Topics</h2></div>
)

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
          </ul>
          <hr />
          {/* http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a */}
          <Route exact path="/" component={connect(state => state)(Home)} />
          <Route exact path="/about" component={About} />
          <Route exact path="/topics" component={Topics} />
        </div>
      </BrowserRouter>
    );
  }
}

// http://qiita.com/MegaBlackLabel/items/df868e734d199071b883#_reference-863a1e1485bf47f046e5
function mapStateToProps(state) {
  return {
    message:state.hello
  };
}

// https://stackoverflow.com/questions/43350683/react-router-uncaught-typeerror-cannot-read-property-route-of-undefined
// export default withRouter(connect(mapStateToProps)(App))
export default connect(state => state)(App)

3.4 Redux 関連

(1) Action (/actions/index.js)

import { createAction } from 'redux-actions';

export const HELLO = 'HELLO';
export const helloAction = createAction(HELLO);

(2) Reducer

・/reducers/hello.js

import { HELLO } from '../actions';

export default function hello(state="", action) {
    switch (action.type) {
        case HELLO:
            return "hello " + (new Date()).toLocaleTimeString("ja-JP");
        default:
            return state;
    }
}

・/reducers/index.js

import { combineReducers } from 'redux';
import hello from './hello';

const rootReducer = combineReducers({
    hello
});

export default rootReducer;

(4) Saga (/sagas/index.js)

  • Actionが呼び出されたときに動作
  • https://redux-saga.js.org/docs/api/
    • takeEvery : 指定したAction.type のdispatchが発生したとき、第2引数のタスクを実行。タスクの引数にActionが設定される
    • call : 第1引数に実行する関数、以降の引数を指定した関数に渡し、Promiseの完了を待つ
    • put : Actionのdispatchを担当
import { takeEvery } from 'redux-saga';
import {
    HELLO, helloAction
} from '../actions';

export default function* rootSaga() {
    yield* takeEvery(HELLO, helloAction);
}

(5) Store (/store/configureStore.js)

  • http://redux.js.org/docs/basics/Store.html
  • アプリケーションの状態(state)を保持
  • getState()で状態(state)を取得
  • dispatch(action) で状態(state)の変更を許可
  • subscribe(listener) でリスナーを登録
  • createSagaMiddlewareを使用しsagaをReduxに登録
  • rootSaga は起動時に1度だけ実行される
  • loggerはデバッグ用、製品版では不要
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import rootReducer from '../reducers';
import rootSaga from '../sagas';

export default function configureStore(initialState) {
    const sagaMiddleware = createSagaMiddleware();
    const store = createStore(
        rootReducer,
        initialState,
        applyMiddleware(
            sagaMiddleware,
            logger  
        )
    );
    sagaMiddleware.run(rootSaga);
    return store;
}

4.実行

  • 特にアプリケーションとしての意味はない
  • 画面上部のリンクを選択すると、React Router によって、下部のコンテンツが入れ替わる
  • Homeを選択したときに、Homeコンポーネント上のHelloボタンを押すことで、Action 発行、Redux、Redux-Sagaの仕組みを通して、メッセージを表示する

redux_saga_app

React Developer Tools で、構成を確認する。ルートのAppおよびHome コンポーネントが conect 関数により、ラッピングされ、dispatch関数が利用できるようになっているのが見て取れる

redux_saga_structure

http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a
http://qiita.com/MegaBlackLabel/items/df868e734d199071b883#_reference-863a1e1485bf47f046e5

React アプリの骨格を作成したので、それに、ページ遷移を実現する、React Router を適用させる。

React Router のリファレンスは以下。

https://reacttraining.com/react-router/web/guides/philosophy

1.準備

1.1 インストール

npm install --save react-router-dom

2.変更

2.1 ./src/app.js

https://reacttraining.com/react-router/web/example/basic の省略形をapp.jsに組み込んでみる。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Link} from 'react-router-dom';


const Home = () => (
  <div><h2>Home</h2></div>
)
const About = () => (
  <div><h2>About</h2></div>
)
const Topics = () => (
  <div><h2>Topics</h2></div>
)

class App extends Component {
  render() {
    return (
      <Router>
        <div>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
            </ul>  
            <hr/>
            <Route exact path="/" component={Home} />
            <Route exact path="/about" component={About} />
            <Route exact path="/topics" component={Topics} />
        </div>
      </Router>
    );
  }
}

ReactDOM.render(
    <App />,
    document.getElementById('root')
);

3.実行

  • npm run watch
  • npm run start

上記で実行、詳細は以下

http://typea.info/blg/glob/2017/08/react-1.html

リンクをクリックすると、コンポーネントが入れ替わる。

react_router