React-Redux超入門(5)


### 概要
[前回](https://qiita.com/hiro4669/items/5683c70ec8d5c7f10059)までのアプリケーションにルーティングを追加する．ルーティングとは，いわゆる画面遷移とほぼ等価で．SPAになる前のWebアプリケーションで，リンクをクリックするとページ全体が書き換わる処理をSPAで実行する仕組みのことを指す．これを実現するライブラリとして`react-router`が有名なので，それを使ってルーティング機能を組みこんでみる．

### 準備
まず，`react-router`をインストールする

```shell:
>npm install --save react-router
```

これをインストールすると，`BrowswerRouter`, `HashRouter`, `Link`, `Route`などのコンポーネントが使えるようになる．それぞれのコンポーネントを概説する．その前に．SPAのルーティングがどのように行われているか整理してみる．SPAのルーティングでも，URLに関連付けて管理したほうが都合が良い(管理しやすい)．しかし，実際にはSPAなので，画面全体の再描画はしてほしくない．そこで，`react-router`では，ブラウザの **historyAPI** を利用してうまくやっている．どのようにうまくやっているか現時点で正確には理解していないが，とにかく1つのURLに対して1画面を対応付ける，というところを抑えておけば良いと思う．

- BrowserRouter: historyAPIを用いてルーティングを管理するコンポーネント．このコンポーネントを使うと，例えば "http://xxx.com/" に対して`App`, "http://xxx.com/second" に対して`Second`のようにURLとコンポーネントを対応付けることができる．ただ．URLを指定するとブラウザの仕様上サーバにHTTPリクエストが送信されてしまう．そこで，404とかになるとダメなので，このコンポーネントを使う場合はサーバサイドで有効なコンテンツを返すような準備が必要となる(どうやるかは後日調査)

- HashRouter:同じくhistoryAPIを用いてルーティングを管理するコンポーネント．BrowserRouterでは1つのURLに対して1つのコンポーネントを対応付けるが，HashRouterではページ内遷移であるハッシュ("#")を利用して関連付ける．例えば，先ほどの例では "http://xxx.com/#" で`App`, "http://xxx.com/#/second" で`Second`のように関連付ける．なお，"#"をつけるのはこのコンポーネントが勝手にやってくれるので，利用者が意識する必要はない．

- Route: URLとコンポーネントを対応付けるコンポーネント．詳細は具体例を参照のこと
- Link: ルーティングを実行するコンポーネント．要するに．URLにジャンプするリンクを作成する．

`react-router`ではこの他にもコンポーネントを提供しているが，ここでは割愛．

次に，これまでのアプリケーションに切り替えて表示するコンポーネントを追加する．

```javascript:/components/Second.js
import React from 'react';

export default (props) => {
    return (
        <h2>Hello Second</h2>
    );
}

```

ルーティングの確認がしたいだけなので，これで十分．

### BrowserRouter/HashRouterを用いたルーティングの実装


#### BrowserRouterを使ったルーティング
ルーティングはルートである`index.js`に導入する．

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom'; // (1)コンポーネントのインポート



import {createStore} from 'redux';
import {Provider} from 'react-redux';

const store = createStore(appReducer);

function renderApp() {
    ReactDOM.render(
        <Provider store={store}>
            <BrowserRouter>    // (2) BrowserRouterの導入
                <div>
                    <ul>
                        <li><Link to='/'>App</Link></li>   // (3) リンクの作成
                        <li><Link to='/second'>Second</Link></li>
                    </ul>
                    <hr/>
                    <Route exact path='/'       component={App} />    // (4)URLとコンポーネントのペア
                    <Route exact path='/second' component={Second} />
                </div>
            </BrowserRouter>
        </Provider>,
        document.getElementById('root')
    );
}

renderApp();
```
(1)
4つのコンポーネントを`react-router-dom`からインポートする．

(2)
`BrowserRoute`を配置する．既に`Redux`を導入しているので，`Provider`の配下に配置する．

(3)
`Link`コンポーネントを使って，リンクを作成している．この例では，例えば"App"という文字をクリックすると．"http://xxx.com/" にジャンプするようになる．

(4)
`Route`コンポーネントを使って`path`と表示するコンポーネントのペアを作成している．この例から明らかのように，"/"にアクセスされれば`App`が．"/second"にアクセスされれば`Second`が表示されるようになる．なお，`Route`の`exact`属性は，パスが完全一致した時のみルーティングされる，という意味である．`exact=true`であるが，`exact`がbooleanなので，`true`は省略可能．


### HashRouterを使ったルーティング
HashRouterを使った場合は以下のようになる．

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom'; 



import {createStore} from 'redux';
import {Provider} from 'react-redux';

const store = createStore(appReducer);

function renderApp() {
    ReactDOM.render(
        <Provider store={store}>
            <HashRouter>    // ここだけ違う
                <div>
                    <ul>
                        <li><Link to='/'>App</Link></li>  
                        <li><Link to='/second'>Second</Link></li>
                    </ul>
                    <hr/>
                    <Route exact path='/'       component={App} />   
                    <Route exact path='/second' component={Second} />
                </div>
            </BrowserRouter>
        </Provider>,
        document.getElementById('root')
    );
}
renderApp();
```

見ればわかるように，`BrowserRouter`と書いていた部分を`HashRouter`と置き換えるだけ．実際に実行してみると(`npm start`)，ブラウザのURLが "http://localhost:3000/" とか，"http://localhost:3000/#/second" になり，ハッシュ(#)を使っていることがわかる．

このように，ハッシュを使ったURLの生成は．`HashRouter`がよろしくやってくれる．

### React-Router-Reduxを利用したルーティング
ここまでの方法では，ルーティングするために

1. `Route`によるpathとコンポーネントのペアを定義
2. `Link`によってリンクを作成

してルーティングを実現してきた．`Link`の`to`属性に指定するのは特定の文字列であるため，実行結果に応じてルーティング先を切り替える，つまりプログラムで実行時にルーティングを切り替えるようなことができない．そこで，React-Router-Reduxでは，ルーティング情報をStoreのステートで管理しつつ．'push'や'replace'のようなメソッドを利用してルーティングするURLを変更する手段を提供する．


#### 準備
React-Router-ReduxとHistoryをインストールする

```
>npm install --save react-router-redux@next
>npm install --save history
```

`react-router-redux`は5系をインストールする必要があるので，`@next`をインストールする．なお，`npm`でリリースされているライブラリのバージョンを確認するには，

```
>npm info react-router-redux version
```

を実行して確かめると良い．ここで4系だったら`@next`(バージョン指定)をつける．

なお， **インストール済み** のライブラリのバージョンを確認するには，

```shell:
>npm list --depth=0
```

とするとよい．

#### Reducerの合成とActionによるルーティングの実行
React-Router-Reduxでは，ルーティングを行うための`routerReducer`というReducerを持っている．ここまで作ってきたアプリケーションでは，(a)既に`reducer/item.js`に`appReducer`を定義しているので，両者を合成する必要がある．
次に，(b)historyをStoreで管理してルーティングする必要がある．
最後に，`Link`などのコンポーネントを使うだけでなく，(c)アクションで実行するプログラムによって，動的にルーティングを実行したい．(a)~(c)を実現するには，それぞれ

(a). `combineReducer`の実行
(b). `ConnectedRouter`の適用
(c). ミドルウェア(`routerMiddleware`)の適用

をそれぞれ行う必要がある．

##### combineReducerの実行
`combineReducer`は，名前の通り複数のReducerを合成する関数である．`combineReducer`は，引数に複数のReducerを内包したJavaScriptのオブジェクトを受け取る．

```javascript:
combineReducer({
   apps: appReducer,
   router: routerReducer,
})
```
この場合，これまで利用していた`appReducer`と，React-Router-Reduxが提供する`routerReducer`を合成し，関数を返す．
例えばこの結果返ってくる関数は，以下のようになる．

```javascript:
ƒ combination() {
    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    var action = arguments[1];

    if (shapeAssertionError) {
      throw shapeAssertionError;…
```

この後，この結果(生成された関数)を利用して`Store`を生成するが，`store.getState()`としてステートを確認すると，

```javascript:
{
apps: {task: "", tasks: Array(0)}
router : {location: null}
__proto__
:
Object
}
```

となっている．これは，`combineReducer`で複数のReducerを合成すると，それぞれのReducerが管理するステートが混在してしまうため，`combineReducer`に渡した複数のreducerを内包するオブジェクトの名前(appsやrouter)を使って，`combineReducer`が1つのステートにマージした，ということである．大事なところ(だと思う)ので，もう一度何でこんなことをする必要があるのか，ということを考えると，

1. Reducerの役割はStateとActionを受け取って新しいStateオブジェクトを返すことである
2. Reduxでは通常アプリケーション全体で1つのStateを扱う．そのため，原則として1つのReducerに対して1つのStateがあるべきである．
3. 複数Reducerが存在する場合，複数のStateが存在することになってしまい都合が悪い
4. よって，`combineReducer`でそれぞれのReducerで管理するStateに名前をつけ，更にそれをまとめて1つのStateとすることで， **1つしかStateオブジェクトは存在しないというReduxの原則を守りつつ．各Reducerは自身が管理するStateしか知らなくてよい** ということを実現する．

という手順で整理することで納得することができると思う．つまり，`combineReducer`をすることで，擬似的に複数のStateが存在するような感じになる．

[ここ](https://redux.js.org/recipes/structuringreducers/usingcombinereducers)

##### ConnectedRouterの適用

これまで利用していた`BrowserRouter`や`HashRouter`の代わりに`ConnectedRouter`を適用する．`ConnectedRouter`はhistoryを利用してルーティングを管理するため，historyを生成し，Props経由で渡す必要がある．

```jsx:
import createBrowserHistory from 'history/createBrowserHistory';
(省略)

const history = createBrowserHistory();
(省略)
<Provider store={store}>
     <ConnectedRouter history={history}>
(省略)     
```
このように，ConnectedRouterはProviderの直下に配置する．


##### Middlewareの適用
`routerMiddleware`をインポートし，さらにReduxが提供する`applyMiddleWare`関数の引数に実行結果のオブジェクトを渡すことでミドルウェアを適用できる．具体的には以下のようになる．

```
import { applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';

(省略)
applyMiddleware(routerMiddleware(history))
```

この返り値は以下のような関数である．

```javascript
ƒ (createStore) {
    return function () {
      for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      var …
```

なお，複数のミドルウェアを適用する場合には，以下のようにすればよい

```javascript
let middleware = [a, b];
applyMiddleware(...middleware);
```

### 複数のReducerとミドルウェアを適用したStoreの生成
複数のReducerとミドルウェアを適用してStoreを生成するには．以下のようにする．

```
const store = createStore(
    combineReducers({..}),
    applyMiddleware(...middleware)
)
```

以上を踏まえ，複数Reducerとミドルウェアを適用した後のindex.js全体を示す．

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom';



import { createStore,
         combineReducers,
         applyMiddleware
       } from 'redux';
import {routerReducer, routerMiddleware } from 'react-router-redux';
import {ConnectedRouter} from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';


import {Provider} from 'react-redux';

const history = createBrowserHistory();

const rstore = createStore(   // (1) Storeの生成
    combineReducers(
        {
            apps: appReducer,
            router: routerReducer,
        }
    ),
    applyMiddleware(
        routerMiddleware(history)
    )
)

function renderApp() {
    ReactDOM.render(
        <Provider store={rstore}>
            <ConnectedRouter history={history}> // (2)ConnectedRouterの適用
                <div>
                    <ul>
                        <li><Link to='/'>App</Link></li>
                        <li><Link to='/second'>Second</Link></li>
                    </ul>
                    <hr/>
                    <Route exact path='/'       component={App} />
                    <Route exact path='/second' component={Second} />
                </div>
            </ConnectedRouter>
        </Provider>,
        document.getElementById('root')
    );
}

renderApp();
```

ここまでの説明のように，(1)でStoreを生成し，(2)で`ConnectedRouter`を適用している．


#### ラッパーの修正
複数のReducerを合成したことにより．Storeの構成が変更された．そのため，`mapStoreToProps`でStoreから値を取り出してPropsに設定する処理を変更する必要がある．先の説明の通り．全体のStoreは1つで変わらないが，`combineReducer`がそれぞれのReducerで管理するStoreを，名前をつけて合成したからである．

```javascript
combineReducers(
      {
          apps: appReducer,
          router: routerReducer,
      }
)
```

上記のように，今回アプリケーションで用意する`appReducer`には，"apps"という名前がついているので，これまで`tasks`や`task`としてアクセスしていた値に，`apps`を追加する必要がある．具体的には，"container"に配置した`Item3.js`, `ItemList.js`を修正する．

```javascript:container/Item3.js
(省略)
function mapStateToProps({apps}) { (1) appsを受け取る
    const {task, tasks} = apps;   
    return ({
        task,
        tasks
    });
}
(省略)
```

(1)
共通のStoreから`apps`(と名前のつけられたappReducerが管理するStoreオブジェクト)を取り出し．appsに代入している．

```javascript:container/ItemList.js
function mapStateToProps({apps}) { // (1) appsを受け取る
    const {tasks} = apps;
    return ({
        items: tasks
    });
}
```

(1)
`Item3.js`と同様．

ここまでの修正を加えることにより．React-Router-Reduxを導入してこれまでと同じ動作をするアプリケーションが実行できる．

ここまでの全ソースコードを示す．

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items';
import App from './components/App';
import Second from './components/Second';
import {BrowserRouter, HashRouter, Route, Link} from 'react-router-dom';



import { createStore,
         combineReducers,
         applyMiddleware
       } from 'redux';
import {routerReducer, routerMiddleware } from 'react-router-redux';
import {ConnectedRouter} from 'react-router-redux';
import createBrowserHistory from 'history/createBrowserHistory';


import {Provider} from 'react-redux';

const store = createStore(appReducer);

const history = createBrowserHistory();

const rstore = createStore(
    combineReducers(
        {
            apps: appReducer,
            router: routerReducer,
        }
    ),
    applyMiddleware(
        routerMiddleware(history)
    )
)


function renderApp() {
    ReactDOM.render(
        <Provider store={rstore}>
            <ConnectedRouter history={history}>
                <div>
                    <ul>
                        <li><Link to='/'>App</Link></li>
                        <li><Link to='/second'>Second</Link></li>
                    </ul>
                    <hr/>
                    <Route exact path='/'       component={App} />
                    <Route exact path='/second' component={Second} />
                </div>
            </ConnectedRouter>
        </Provider>,
        document.getElementById('root')
    );
}
renderApp();
```

```javascript:reducers/items.js
const initialState = {
    task: '',
    tasks: [],
};

function appReducer(state = initialState, action) {
    switch(action.type) {
        case 'INPUT_TASK':
            return {
                ...state,
                task: action.payload.task,
            };
        case 'ADD_TASK':
            const nl = state.tasks.concat([action.payload.task]);
            return {
                ...state,
                tasks: nl
            };
        default:
            return state;
    }
}

export default appReducer;
```

```javascript:actions/items.js

export const inputTask = (task) => {
    return ({
        type: 'INPUT_TASK',
        payload: {
            task
        }
        });
}

export const addTask = (task) => {
    return ({
        type: 'ADD_TASK',
        payload: {
            task
        }
    });
}
```

```javascript:containers/Item3.js
import {connect} from 'react-redux';
import Input3 from '../components/Input3';
import {inputTask, addTask} from '../actions/items';

function mapStateToProps({apps}) {
    console.log("mapStateToProps in input3");
    console.log(apps);
    const {task, tasks} = apps;

    return ({
        task,
        tasks
    });
}


function mapDispatchToProps(dispatch) {
    return ({
        inputTask(task) {
            dispatch(inputTask(task));
        },
        addTask(task) {
            dispatch(addTask(task));
        },
    });
}

export default connect(mapStateToProps, mapDispatchToProps)(Input3);

```

```javascript:containers/ItemList.js
import {connect} from 'react-redux';
import ItemList from '../components/ItemList';


function mapStateToProps({apps}) {
    const {tasks} = apps;
    return ({
        items: tasks
    });
}
export default connect(mapStateToProps)(ItemList);
```

```jsx:components/App.js
import React, { Component } from 'react';
import './App.css';

import Input3 from '../containers/Input3';
import ItemList from '../containers/ItemList';

class App extends Component {
  constructor(props) {
      super(props);
  }

  render() {
    return (
        <div>
          <h1>Todo App</h1>
            <Input3/>
          <ul>
            <ItemList />
          </ul>
        </div>
    );
  }
}

export default App;
```

```jsx:components/Input3.js
import React, {Component} from 'react';

class Input3 extends Component {
    constructor(props) {
        super(props);
        this.addItem = this.addItem.bind(this);
    }

    addItem() {
        this.props.addTask(this.props.task);
    }

    render() {
        return (
            <div>
                <input type="text" onChange={(e) => {this.props.inputTask(e.target.value)}} />
                <button onClick={this.addItem}>Add</button><br/>
            </div>
        )
    }
}
export default Input3;
```

```jsx:components/Item.js
import React from 'react'

const Item = (props) => {
    return (
        <li>{props.name}</li>
    )
}

export default Item;
```

```jsx:components/ItemList.js
import React from 'react';
import Item from './Item';

const ItemList = ({items}) => {
    let list = [];
    for (var i = 0; i < items.length; ++i) {
        list.push(<Item key = {i} name={items[i]} />);
    }
    return list;
}
export default ItemList;
```

```jsx:components/Second.js
import React from 'react';

export default (props) => {
    return (
        <h2>Hello Second</h2>
    );
}
```

### プログラムでルーティングを制御
`routerMiddleware`を適用したことにより，プログラムでルーティングが制御できるようになる．そこで，Item3.jsに1つボタンを追加し，"ボタンを押したらSecondにルーティングする"，ということをやってみる．
具体的にやることは，

1. components/Input3.jsにボタンを追加
2. 追加したボタンのイベントハンドラでProps経由で渡される関数を実行
3. containers/Input3.jsの`mapDispatchToProps`で，2にわたす関数を追加．
4. 3で渡す関数の中で，`dispatch(push("second"))`を実行

のようにする．
手順4でした`dispatch(push("/second"))`を見ればわかるように，プログラムでルーティングを制御するには，`dispatch`を使って変更をReducerに伝える．その際，`push`を使ってhistoryを変更する．historyに渡す文字列が，これまで`<Link to>`を使って渡していた文字列となる．

関連するファイルだけ示す．上記の全ソースコードから当該ファイルを入れ替えれば動く(はず)．

```jsx:components/Input3.js
import React, {Component} from 'react';

class Input3 extends Component {
    constructor(props) {
        super(props);
        this.addItem = this.addItem.bind(this);
    }

    addItem() {
        this.props.addTask(this.props.task);
    }

    render() {
        return (
            <div>
                <input type="text" onChange={(e) => {this.props.inputTask(e.target.value)}} />
                <button onClick={this.addItem}>Add</button><br/>
                <button onClick={(e) => {this.props.gotoSecond()}}>Second</button> // 追加(手順1と2)
            </div>
        )
    }
}
export default Input3;
```

```javascript:containers/Input3.js
import {connect} from 'react-redux';
import Input3 from '../components/Input3';
import {inputTask, addTask} from '../actions/items';
import { push } from 'react-router-redux'; // pushのインポート

function mapStateToProps({apps}) {
    const {task, tasks} = apps;
    return ({
        task,
        tasks
    });
}


function mapDispatchToProps(dispatch) {
    return ({
        inputTask(task) {
            dispatch(inputTask(task));
        },
        addTask(task) {
            dispatch(addTask(task));
        },
        gotoSecond() { // 追加(手順3)
            dispatch(push("/second")); // 追加(手順4)
        },
    });
}

export default connect(mapStateToProps, mapDispatchToProps)(Input3);
```

以上，React-Routerを利用したルーティングとReact-Router-Reduxを利用したルーティングを簡単に(?)書いてみた．