React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用する
フロントエンドにReactを使ったアプリを作成したいが、React は Viewしか担当していないこともあり、なかなか敷居が高い。ステップをおって、アプリケーションのスケルトンを作成することを目的とする。
で、Reactの開発環境の構築をおこない、
React の単純なサンプルに React Router を適用する
で、React Router を導入して、ページ遷移を適用した。
今回、Redux および、Redux-Saga を適用して、データの取り回し、および非同期タスクの取り扱いの枠組みを導入する。
以下、公式リファレンス
参考
概念的にも、非常にややこしい。以下のサイトのサンプルを参照、内容をメモしながら進める。
1.Redux
1.1 アーキテクチャ
- FacebookのFluxアーキテクチャに基づいて、設計された
- Flux アーキテクチャ : Action -> Dispatcher -> Store -> View(React) の4部品からなり、データの流れは1方向
-
Redux アーキテクチャ: Action -> Reducers -> Store の3部品からなり、Flux同様データは一方向に流れる
-
Store(アプリケーションの状態管理) は、アプリケーションに1つのみ
-
Actionが発生した際、Reducerを使用して、Storeの状態(State)を変更する
-
Storeの用意するAPIを利用することで、Viewは、state更新の購読とstateの取得を行う
1.2 3原則
- Single source of truth (信頼できる唯一の情報源) : アプリケーション全体のStateは、一つのStoreに格納
- State is read-only (state は 読み取り専用) : State 変更は、必ず Action 経由
- 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 とは
- この図は、永久保存版じゃなかろうか。よくわかる。
3.サンプル
という概要を踏まえて、
状態 に以下の変更を施し、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
- Provider がStoreを保持している
- import “babel-polyfill“; で、Generator を利用できるようにする。
- 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コンポーネント化
- この図をイメージ
- 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)
- http://redux.js.org/docs/basics/Actions.html
- Actionは、Action名と状態値を持つ、単なるオブジェクト
- アプリケーションからStoreにデータを送るペイロード
- import { createAction } from 'redux-actions';
- export const HELLO = 'HELLO';
- export const helloAction = createAction(HELLO);
(2) Reducer
- http://redux.js.org/docs/basics/Reducers.html
- アプリケーションの状態(state)の変化させる
- 受け取ったActionのタイプにより、Actionの値を用いてStoreのstateを更新
・/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の仕組みを通して、メッセージを表示する
React Developer Tools で、構成を確認する。ルートのAppおよびHome コンポーネントが conect 関数により、ラッピングされ、dispatch関数が利用できるようになっているのが見て取れる
http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a
http://qiita.com/MegaBlackLabel/items/df868e734d199071b883#_reference-863a1e1485bf47f046e5
- React 開発の全体像を把握しつつ開発環境を整える
- React の単純なサンプルに React Router を適用する
- React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用する
- React の単純なサンプルに React Router を組み込んだものに Redux-Saga を適用したものからAjax通信を組み込む
- React環境->React Router->Redux-Saga->SuperAgent に Bootstrapを適用する