Reactサンプル：redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させる。（後編）

[Reactサンプル：redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させる。（前編）](https://qiita.com/jun68ykt/items/f2dc1f7c076c76188834)
からの続きです。

# 6. redux-sagaからAPIを呼びだす。

## 6.1 redux-sagaを追加

　`GET_ADDRESS_REQUESTED`アクションが送出されたらAPIをリクエストしてその結果を待ち、結果が返ってきたら何かをするという同期処理を実現するのに `redux-saga` を使います。

```
$ yarn add redux-saga
```

上記により、以下が `package.json` に追加されました。

```
"redux-saga": "^0.16.0"
```

## 6.2 getAddress sagaの実装

`src/ducks/address.js` の冒頭に以下を追加します。

```javascript
import { takeLatest, put } from 'redux-saga/effects';
import api from "../api/postcode-jp";
```

`GET_ADDRESS_REQUESTED`アクションを受け取ったら、[3.1 郵便番号から住所を取得](https://qiita.com/jun68ykt/items/f2dc1f7c076c76188834#31-%E9%83%B5%E4%BE%BF%E7%95%AA%E5%8F%B7%E3%81%8B%E3%82%89%E4%BD%8F%E6%89%80%E3%82%92%E5%8F%96%E5%BE%97) で用意しておいたAPIを呼び、その結果が

 - 成功なら `GET_ADDRESS_SUCCEEDED`
 - 失敗なら `GET_ADDRESS_FAILED`
 
を発生させるsagaを以下のように実装して、これを `src/ducks/address.js` に追加します。

```javascript
// Sagas
function* getAddress(action) {
  const res = yield api.getAddress(action.payload.zipCode);
  if (res.data && res.data.length > 0) { // 成功： 指定された郵便番号に該当する住所が存在した。
    yield put({
      type: GET_ADDRESS_SUCCEEDED,
      payload: {
        zipCode: action.payload.zipCode,
        address: res.data[0].allAddress,
        error: false,
      }
    });
  } else { // 失敗： 指定された郵便番号に該当する住所が存在しなかった。
    const message = res.validationErrors ? res.validationErrors[0].message : null;
    yield put({
      type: GET_ADDRESS_FAILED,
      payload: new Error(message),
      error: true,
    });
  }
};

function* watchLastGetZipData() {
  yield takeLatest(GET_ADDRESS_REQUESTED, getAddress);
}

export const sagas = [
  watchLastGetZipData,
];

```

## 6.3 reduxにsagaMiddlewareを追加

上記で作成した saga をreduxと連携させる処理を `src/index.js` に追加します。

```jsx
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { all } from 'redux-saga/effects';
import { InputForm, Success, Failure } from './containers'
import address, { initialState, sagas } from "./ducks/address";

const allSagas = [ ...sagas, ];

function* rootSaga() {
  yield all(allSagas.map(f => f()));
}

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  address,
  initialState,
  applyMiddleware(sagaMiddleware, createLogger()),
);

sagaMiddleware.run(rootSaga);

const App = () => (
  <BrowserRouter>
    <Provider store={store}>
      <Switch>
        <Route exact path="/" component={ InputForm } />
        <Route exact path="/success" component={ Success } />
        <Route exact path="/failure" component={ Failure } />
      </Switch>
    </Provider>
  </BrowserRouter>
);

render(<App />, document.querySelector('#app'));

```

　上記の修正後、フォームから住所を調べたい郵便番号を入力して、送信ボタンをクリックすると、以下のようになります。

- `１５４0043`　を入力して、送信をクリックすると、レスポンスの内容から成功と判断され、`GET_ADDRESS_SUCCEEDED`が送出される。

<img width="1398" alt="スクリーンショット 2018-05-13 14.44.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/1eaf99de-00a4-bcd7-6160-023f519436fa.png">


- `9999999999`を入力して、送信をクリックすると、APIのレスポンスから失敗と判断され、`GET_ADDRESS_FAILED`が送出される。

<img width="1398" alt="スクリーンショット 2018-05-13 14.42.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/d36278dd-4055-a5b0-673c-1b46c9e90bd7.png">


　🐾**ここまでの追加・修正コミット： [SagaからAPIをリクエストして結果に応じたアクションを送出](https://github.com/jun68ykt/saga-and-router4/commit/05e04980246dba68d935a365e0421511a76217eb)**


# 7. APIの結果に応じた画面遷移の実装

　前節までで、入力された郵便番号を付加してAPIを呼び、その結果に応じたアクションを送出し、意図どおりにstateが変更されることを確認できました。次に、本稿の主題である、APIの成功時に`/success` に遷移し、失敗時には `/failure` に遷移させる処理を追加していきます。

## 7.1 参考にした GitHub issue

　`redux-saga` から何らかの非同期処理（本スパイクでは `fetch()` によるWebAPIの呼び出し）を行い、それが終わってから画面遷移させるには、どういう実装方法が定番なのかを調べたところ、[ReactTraining/react-router](https://github.com/ReactTraining/react-router) のissue 

　　Using v4 with redux-saga
　　https://github.com/ReactTraining/react-router/issues/3972

の下記３点のコメントを参考にしてスパイクしました。

- [trabianmatt commented on 4 Oct 2016](https://github.com/ReactTraining/react-router/issues/3972#issuecomment-251189856)
- [pshrmn commented on 5 Dec 2016](https://github.com/ReactTraining/react-router/issues/3972#issuecomment-264805667)
- [nateq314 commented on 10 Feb 2017](https://github.com/ReactTraining/react-router/issues/3972#issuecomment-278684003)

## 7.2 アクションにリクエスト処理後に遷移するページを追加する。

`GET_ADDRESS_REQUESTED`アクションに、リクエスト処理後に遷移するページを追加します。

[FSAの説明](https://github.com/redux-utilities/flux-standard-action)に、action の追加プロパティとして

> meta
>
> The optional meta property MAY be any type of value. It is intended for any extra information that is not part of the payload.

とありますので、この `meta` に成功時および失敗時の遷移先を与えることにします。

`src/ducks/address.js` のアクションクリエータ`getAddressRequested`　の引数に `meta` を追加して以下のようにします。

```javascript
// Action Creators
export const getAddressRequested = (zipCode, meta) => (
  {
    type: GET_ADDRESS_REQUESTED,
    payload: { zipCode },
    meta
  }
);
```

`src/containers/form/InputForm.jsx` の `handleSubmit` で、アクションをdispatchする際に、上記の `meta` として
成功時の遷移先（pageOnSuccess)と、失敗時の遷移先（pageOnFailure)を指定し、以下のようにします。

```javascript
  handleSubmit(e) {
    e.preventDefault();
    const meta = {
      pageOnSuccess: '/success',
      pageOnFailure: '/failure'
    };
    this.props.dispatch(getAddressRequested(this.state.zipCode, meta));
  }
```


## 7.3 saga に historyを渡す

次に、[react-router/issues/3972#issuecomment-264805667](https://github.com/ReactTraining/react-router/issues/3972#issuecomment-264805667) のコード例：

```javascript

import history from './history'

export function* signin(api, { payload }) {
  const response = yield call(api.auth.signin, payload);

  if (response.ok) {
    yield put(AuthActions.signinSuccess({ user: response.data.data }));
    yield call(history.push, '/dashboard');

```

と同様に `history.push` をsaga の中で使うことで画面遷移させることを考えます。　saga に `history`を渡す実装としては、[react-router/issues/3972#issuecomment-251189856](https://github.com/ReactTraining/react-router/issues/3972#issuecomment-251189856) で、saga に `router`を渡しているコードを参考にします。

　まず、 モジュールhistory を追加します。

```shell-session
$ yarn add history
```
以下が `package.json` に追加されます。

```json
"history": "^4.7.2",
```

`src/index.js` の `BrowserRouter` を `Router` に変更し、明示的に作成したhistoryを持たせます。
そして、この `history` を `sagaMiddleware.run` させるときに与えて、sageで使えるようにします。

```jsx
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { all } from 'redux-saga/effects';
import createHistory from 'history/createBrowserHistory';
import { InputForm, Success, Failure } from './containers';
import address, { initialState, sagas } from "./ducks/address";

const allSagas = [ ...sagas, ];

function* rootSaga(context) {
  yield all(allSagas.map(f => f(context)));
}

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  address,
  initialState,
  applyMiddleware(sagaMiddleware, createLogger()),
);

const history = createHistory();

sagaMiddleware.run(rootSaga, { history });

const App = () => (
  <Router history={history}>
    <Provider store={store}>
      <Switch>
        <Route exact path="/" component={ InputForm } />
        <Route exact path="/success" component={ Success } />
        <Route exact path="/failure" component={ Failure } />
      </Switch>
    </Provider>
  </Router>
);

render(<App />, document.querySelector('#app'));
```

## 7.4 sagaでhistory.pushして画面遷移させる

　次に、saga のほうで、`{ history }`を受け取って、これを画面遷移のために使うようにします。
`src/ducks/address.js` の冒頭で、`redux-saga/effects` から `call` も import します。

```javascript
import { takeLatest, put, call } from 'redux-saga/effects';
```

`call` と、 `src/index.js`から渡された `{ history }` を使うことで、
以下のように`getAddress`を修正して、画面遷移を実装します。

```javascript
// Sagas
function* getAddress(context, action) {
  const meta = action.meta || {};
  const res = yield api.getAddress(action.payload.zipCode);
  if (res.data && res.data.length > 0) { // 成功： 指定された郵便番号に該当する住所が存在した。
    yield put({
      type: GET_ADDRESS_SUCCEEDED,
      payload: {
        zipCode: action.payload.zipCode,
        address: res.data[0].allAddress,
        error: false,
      }
    });
    if (meta.pageOnSuccess)
      yield call(context.history.push, meta.pageOnSuccess);
  } else { // 失敗： 指定された郵便番号に該当する住所が存在しなかった。
    const message = res.validationErrors ? res.validationErrors[0].message : null;
    yield put({
      type: GET_ADDRESS_FAILED,
      payload: new Error(message),
      error: true,
    });
    if (meta.pageOnFailure)
      yield call(context.history.push, meta.pageOnFailure);
  }
};

function* watchLastGetZipData(context) {
  yield takeLatest(GET_ADDRESS_REQUESTED, getAddress, context);
}
```

　上記の修正後、フォームから `1500043` および `9999999999` を入力して、送信ボタンをクリックすると、以下のように意図したとおりに画面遷移します。

### 7.4.1 成功時

<img width="1157" alt="スクリーンショット 2018-05-13 15.55.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/e17062da-1538-4770-20a5-b75d8ab9042c.png">
　上記で、送信をクリックすると、以下のように `/success` に遷移して、 `Success`コンテナが renderされる。

<img width="1157" alt="スクリーンショット 2018-05-13 15.57.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/11b22bf3-a2b1-6f11-c3f8-807a3417ec90.png">

### 7.4.2 失敗時

<img width="1157" alt="スクリーンショット 2018-05-13 16.00.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/839d5fed-09a4-7b0c-e916-7c9c6dc094c6.png">
上記で、送信をクリックすると、以下のように `/failure` に遷移して、 `Failure`コンテナが renderされる。

<img width="1157" alt="スクリーンショット 2018-05-13 16.01.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/7dee4a7e-c66c-7998-6e6a-0528474c86a7.png">


　🐾**ここまでの追加・修正コミット： [sagaとhistoryによる画面遷移](https://github.com/jun68ykt/saga-and-router4/commit/9b823bf6bf2a3fc9e432cafb63a15d672afd9adc)**

　以上で、スパイクの主題であった、
　**WebAPIをリクエストし、その結果に応じた画面遷移をredux-sagaとreact-routerで実現するには？**
という問題は解決されました。ですが、もう少し手を入れてアプリらしくしてみます。


# 8. 画面表示の改良

以下の5点を改良します。

- 成功時、`/success` にて該当した住所を表示

- 失敗時、`/failure` にてエラーメッセージを表示（有効なエラーメッセージがレスポンスに含まれる場合）

- 成功時、失敗時ともに、フォームにもどるリンクを追加

- もどるリンクから、フォームに戻ったときに、前回入力した郵便番号をテキスト入力のデフォルト値として表示

- フォームで送信ボタンをクリックされた後、APIのレスポンス待ちの間、ローディングアイコンを表示

以下、上記の改良を施したソースです。

## 8.1 結果表示画面

`src/containers/Success.jsx`

```jsx
import React, { Component } from 'react';
import { connect } from "react-redux";
import { Link } from 'react-router-dom';

class Success extends Component {

  render() {
    return(
      <div>
        <div className="message success">住所の取得に成功しました。</div>
        <br />
        <div>入力された値：<span className="em">{this.props.zipCode}</span></div>
        <div>取得された住所：<span className="em">{this.props.address}</span></div>
        <br />
        <Link to='/'>もどる</Link>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state
}

export default connect(mapStateToProps)(Success);
```

`src/containers/Failure.jsx`

```jsx
import React, { Component } from 'react';
import { connect } from "react-redux";
import { Link } from 'react-router-dom';

class Failure extends Component {

  render() {
    return(
      <div>
        <div className="message failed">住所の取得に失敗しました。</div>
        <br />
        <div>入力された値：<span className="em">{this.props.zipCode}</span></div>
        {this.props.error && <div>エラーメッセージ：<span className="em">{this.props.error}</span></div>}
        <br />
        <Link to='/'>もどる</Link>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state
}

export default connect(mapStateToProps)(Failure);
```

## 8.2 入力フォーム画面

`src/containers/forms/InputForm.jsx`

```jsx
import React, { Component } from 'react';
import { connect } from "react-redux";
import { getAddressRequested } from "../../ducks/address";

class InputForm extends Component {
  constructor(props) {
    super(props);
    this.state = { zipCode: this.props.zipCode || '' };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(e) {
    this.setState({ zipCode: e.target.value.replace(/[^0-9]/, '') });
  }

  handleSubmit(e) {
    e.preventDefault();
    const meta = {
      pageOnSuccess: '/success',
      pageOnFailure: '/failure'
    };
    this.props.dispatch(getAddressRequested(this.state.zipCode, meta));
  }

  render() {
    return(
      <div className="form">
        <form onSubmit={this.handleSubmit}>
          <input
            type="text"
            name="zipCode"
            value={this.state.zipCode}
            placeholder="半角数字で入力"
            onChange={this.handleChange} />
          { this.props.apiIsProcessing ?
            <span className="loading">処理中･･･
              <img alt='loading' className="loading-icon" src='/img/loading.gif'/></span>  :
            <input type="submit" value="送信" /> }
        </form>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state
}

export default connect(mapStateToProps)(InputForm);
```

上記で、ローディングアイコン `loading.gif` は以下のようなものです。

![loading.gif](https://qiita-image-store.s3.amazonaws.com/0/74659/b170dd7b-363a-0c62-9bf3-a19647268c4a.gif)

この画像を `public/img/` に保存します。

## 8.3 スタイルシート

最後に、`public/css/style.css` にいくつか追記して、以下のようにします。

```css
div#app {
  padding-top: 16px;
  padding-left: 12px;
}

input[type="text"] {
  font-size: 14pt;
  margin-right: 10px;
  padding: 3px;
}

input[type="submit"] {
  font-size: 12pt;
  background-color: #CCC;
  color: #000;
}

.form { position: relative; }
.loading { color: #888; }
.loading-icon { height: 32px; position: absolute; bottom: 0;}
.message { padding-left: 10px; font-size: 16pt; color: white; }
.success { background-color: #005B98; }
.failed { background-color: #F20000; }
.em { font-weight: bold; }
```

　🐾**ここまでの追加・修正コミット： [画面表示の改良](https://github.com/jun68ykt/saga-and-router4/commit/336550e660f0216192f53d680f3ce229426a1b2a)**


## 8.4 動作確認

前節までの修正によって、以下のように表示されます。

### 8.4.1 成功時

<img width="1283" alt="スクリーンショット 2018-05-14 0.45.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/20842071-5a0d-db6f-3681-6c1e1d2097d5.png">

送信ボタンをクリックすると、以下が表示される。

<img width="1283" alt="スクリーンショット 2018-05-14 0.46.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/61e27cce-766f-7f83-394e-ec6fbc2e78b6.png">


### 8.4.2 失敗時

<img width="1283" alt="スクリーンショット 2018-05-14 0.46.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/2f1ad2b1-ef4f-e28b-933f-84a549c7c41e.png">

送信ボタンをクリックすると、以下が表示される。

<img width="1283" alt="スクリーンショット 2018-05-14 0.46.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/6af53e27-c412-e5c0-d90d-218eb49751bf.png">


# おわりに

　本稿では、redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させるスパイクを行い、
とりあえず意図どおりに動くものができました。
　最後までお読みいただき、ありがとうございました。


　👉[前編に戻る](https://qiita.com/jun68ykt/items/f2dc1f7c076c76188834)