React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用する

フロントエンドに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

  1. React 開発の全体像を把握しつつ開発環境を整える
  2. React の単純なサンプルに React Router を適用する
  3. React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用する
  4. React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用したものからAjax通信を組み込む
  5. React環境->React Router->Redux-Saga->SuperAgent に Bootstrapを適用する