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

# 更新情報

- 2020/04/12: 使用するNodeモジュールを最新にし、また住所検索APIの仕様変更に追従しました。
  - 👉 [Reactサンプル：redux-sagaとreact-router v4（をv5にしたうえ）でWebAPIの結果に応じて画面遷移させる。（更新情報）](https://qiita.com/jun68ykt/items/86a0bc2ffcfcd8e9f70c)

- 2018/05/14 当記事、初出

なお、以下の「**はじめに**」から始まる本文は、2018/05/14の初出のときから修正していません。

# はじめに

## TL;DR

　React によるフロント開発の作例です。ソースコードは以下から取得できます。

　https://github.com/jun68ykt/saga-and-router4

## 背景

　目下、reactとredux を使ったSPA開発に従事しています。その開発途上で、

　**WebAPIをリクエストし、その結果に応じた画面遷移をredux-sagaとreact-routerで実現するには？**

という課題があり、これを解決するためのスパイクとして簡単なWebフロントを作りました。そのソースコードと参考にした周辺情報を共有すべく、この投稿を書きました。

## 本投稿の構成

本投稿は、以下のように前編と後編に分かれています。

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

## 対象読者

- React初心者〜中級のフロントエンド開発者を対象読者として想定
- React初心者にも分かりやすいように、スクラッチから段階をふんでコードを追加していきます。
    - ただし、ここでいう「React初心者」とは、Javascript初心者を意味していません。特に redux-saga を使いこなすには、ES6から導入されたGenerator の理解が必須というのが私の考えです。
- コード追加、修正を行ったGitコミットを各段階の終わりに示します。
- React中〜上級者の方へ： 本投稿の主題（最も関心のある課題）の解決をしているのは、後編の以下のセクションです。
  - [7.3 saga に historyを渡す](https://qiita.com/jun68ykt/items/541cc8247900e126ac5b#73-saga-%E3%81%AB-history%E3%82%92%E6%B8%A1%E3%81%99)
  - [7.4 sagaでhistory.pushして画面遷移させる](https://qiita.com/jun68ykt/items/541cc8247900e126ac5b#74-saga%E3%81%A7historypush%E3%81%97%E3%81%A6%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E3%81%95%E3%81%9B%E3%82%8B)

## 本稿で作成するWebフロントの仕様

- 入力された郵便番号から住所を検索して表示します。

- 最終的に出来上がる画面と機能は、[8.4 動作確認](https://qiita.com/jun68ykt/items/541cc8247900e126ac5b#84-%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D) の画面キャプチャのとおりです。
- 仕様の概要は以下です。

1. **画面構成**
  - 以下の２つの画面から構成される。
      - 入力フォーム画面：　郵便番号を入力するテキスト入力と送信ボタンを持つフォーム
      - 結果表示画面：入力フォームで入力された郵便番号で住所を検索した結果を表示
2. **各画面のURL**
  - 入力フォーム画面： /
  - 結果表示画面：
      - 成功の場合： /success
      - 失敗の場合： /failure
3. **利用するAPI**
  - 郵便番号による住所検索サービスとして、[PostCodeJP様提供のWebAPI](https://postcode-jp.appspot.com/)を利用させて頂きます。


## 利用するNodeモジュール

以下のモジュールを利用します。

 - react および以下のモジュール
    - react-dom
    - react-redux
    - react-router-dom
 - redux および以下のmidllewareモジュール
  - redux-logger
  - redux-saga
 - history

## 参照するブログ記事、GitHubレポジトリなど

　本稿では、以下の記事、GitHubレポジトリのREADME、issue等を、適宜参照します。

- [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 
- [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action#flux-standard-action)
- [Ducks: Redux Reducer Bundles](https://github.com/erikras/ducks-modular-redux#ducks-redux-reducer-bundles)
- [Using v4 with redux-saga](https://github.com/ReactTraining/react-router/issues/3972)


# 1.　開発環境の確認・準備

## 1.1 開発マシン

　本稿のコードの作成、動作確認は以下で行いました。

- iMac (Retina 4K, 21.5-inch, 2017)
- macOS High Sierra バージョン 10.13.4

## 1.2　node, yarn および create-react-app のバージョン

今回使用した、`node`, `yarn` および `create-react-app` のバージョンは以下です。
(以下で $ はシェルのプロンプトです。）

```shell-session
$ pwd
/Users/jun68ykt/ReactSpikes
$ node --version
v10.1.0
$ yarn --version
1.6.0
$ create-react-app --version
1.5.2
$
```

## 1.3 新規アプリの作成

　`create-react-app` で新規アプリ `saga-and-router4` を作成し、`yarn start` して、ブラウザにデフォルト画面が表示されることを確認します。

```shell-session
$ create-react-app saga-and-router4

Creating a new React app in /Users/jun68ykt/ReactSpikes/saga-and-router4.

（・・・ 略）

$ cd saga-and-router4
$ yarn start
```
問題なく表示されるのを確認したら、Ctrl-C  でアプリを終了させます。


```shell-session

（・・・ 略）


Note that the development build is not optimized.
To create a production build, use yarn build.

^C
$ 
```
スクラッチから作るため、不要なファイルを削除または空にします。

```shell-session
$ pwd
/Users/jun68ykt/ReactSpikes/saga-and-router4
$ rm -f public/favicon.ico
$ rm -f public/manifest.json
$ rm -f src/App.*
$ rm -f src/logo.svg
$ rm -f src/registerServiceWorker.js
$ rm -f src/index.css
$ > src/index.js
$ cat src/index.js
$
```

`public/index.html` を以下のように修正します。

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>A sample of redux-saga and react-router v4</title>
    <link href="css/style.css" rel="stylesheet">
  </head>
  <body>
    <div id="app" />
  </body>
</html>
```

`public/css/style.css` を以下のように空のファイルとして作成しておきます。

```shell-session
$ mkdir public/css
$ > public/css/style.css
$ cat public/css/style.css
$
```
　
　この状態で、 `yarn start` して、ブラウザの開発ツールのConsole にエラーが表示されることなく、白紙のページが表示されることを確認できたら準備完了です。

　🐾**ここまでの追加・修正コミット： [最初のコミット](https://github.com/jun68ykt/saga-and-router4/commit/efd12d537150aee62f9101604390e8307c363438)**


# 2. 各画面のコンポーネントとルーティング設定の実装

## 2.1 react-router-domを追加

　各画面に対応するコンポーネントを作成して、画面のURLと対応させるため、`react-router-dom` を追加します。

```shell-session
$ yarn add react-router-dom
```
package.json の dependencies に以下が追加されました。

```json
"react-router-dom": "^4.2.2",
```
## 2.2 各画面コンポーネント
　
　各画面に対応するコンポーネントを作成します。
　これらは、[Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) で説明されているContainer Componentsになるものなので、`src/` の下に `containers/` フォルダを作り、ここに以下の３点を作成します。

### 2.2.1 入力フォーム：

`src/containers/forms/InputForm.jsx`

```jsx
import React, { Component } from 'react';

class InputForm extends Component {
  render() {
    return <div>InputForm</div>;
  }
}

export default InputForm;
```

### 2.2.2 結果表示（成功）

`src/containers/results/Success.jsx`

```jsx
import React, { Component } from 'react';

class Success extends Component {
  render() {
    return <div>Success</div>;
  }
}

export default Success;
```


### 2.2.3 結果表示（失敗）

`src/containers/results/Failure.jsx`

```jsx
import React, { Component } from 'react';

class Failure extends Component {
  render() {
    return <div>Failure</div>;
  }
}

export default Failure;
```

### 2.2.4 src/containers/index.js

さらに、上記３点をまとめて export するために、 `src/containers/index.js` を以下のように作成します。

```javascript
import InputForm from './forms/InputForm';
import Success from './results/Success';
import Failure from './results/Failure';

export { InputForm, Success, Failure };
```


## 2.3 src/index.js にルーティング設定を書く

　本稿で作るWebフロントの仕様に記載したURLと、上記で作成した画面に対応するコンテナとをマッピングさせるため、`src/index.js` を以下のように書きます。

```jsx
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { InputForm, Success, Failure } from './containers'

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

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

ここまで出来た状態で、 `yarn start` すると、以下の画面キャプチャ

<img width="639" alt="スクリーンショット 2018-05-13 21.01.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/090292cd-ce22-ef6e-a670-ec74a477958f.png">
<img width="639" alt="スクリーンショット 2018-05-13 21.02.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/2e788f5a-91bb-7439-ab3d-d66228160a1b.png">
<img width="639" alt="スクリーンショット 2018-05-13 21.02.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/cc2d7674-dcd9-341a-47c5-2acd6d1569a5.png">

のように、

- 初期画面( http://localhost:3000/ )では **InputForm** と表示され、
- アドレスバーに、 http://localhost:3000/success と入力すると、**Success** と表示され、
- http://localhost:3000/failure と入力すると、画面に **Failure** と表示される


ことが確認できます。

　🐾**ここまでの追加・修正コミット： [各画面のコンテナを作成し、ルーティングを設定](https://github.com/jun68ykt/saga-and-router4/commit/b23a69b1dfdf0db00c2615aa478d8968d59a0477)**


# 3. 郵便番号API

　[本稿で作るWebフロントの仕様](https://qiita.com/jun68ykt/items/f2dc1f7c076c76188834#%E6%9C%AC%E7%A8%BF%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8Bweb%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%81%AE%E4%BB%95%E6%A7%98)に記載したように、郵便番号による住所検索サービスとして、[PostCodeJP様提供のWebAPI](https://postcode-jp.appspot.com/)を利用させて頂きます。

## 3.1 郵便番号から住所を取得

　郵便番号から住所情報を取得する api リクエストとレスポンス取得は、fetch を使います。
`src/api/` ディレクトリを作成し、以下の postcode-jp.js を作成します。

`src/api/postcode-jp.js`

```javascript
const getAddress = (zipCode) => {
  const url = `https://postcode-jp.appspot.com/api/postcode?general=true&office=true&postcode=${zipCode}`;
  return fetch(url)
    .then(res => res.json())
    .catch((e) => { console.log(`ERROR: ${e.message}`)});
};

const apis = { getAddress };

export default apis;
```

　🐾**ここまでの追加・修正コミット： [郵便番号API追加](https://github.com/jun68ykt/saga-and-router4/commit/6ab9da2e1c12a44d9dc0db73f67433e858313c70)**

# 4. Redux構成要素の実装

　`redux` の構成要素を実装していきます。以下の２つを設計のガイドラインとします。

- [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action#flux-standard-action)
- [Ducks: Redux Reducer Bundles](https://github.com/erikras/ducks-modular-redux#ducks-redux-reducer-bundles)

　上記の[Ducks: Redux Reducer Bundles](https://github.com/erikras/ducks-modular-redux#ducks-redux-reducer-bundles)の[サンプル](https://github.com/erikras/ducks-modular-redux#example)にならい、`src/ducks`というフォルダを作り、ここに`address.js`というファイルを作成して、この一つのファイルにアクション、リデューサ、アクションクリエータを実装します。

`src/ducks/address.js`

```javascript
// Actions
const GET_ADDRESS_REQUESTED = 'saga-and-router4/address/GET_ADDRESS_REQUESTED'; // 住所の取得を要求した。
const GET_ADDRESS_SUCCEEDED = 'saga-and-router4/address/GET_ADDRESS_SUCCEEDED'; // 住所の取得が成功した。
const GET_ADDRESS_FAILED = 'saga-and-router4/address/GET_ADDRESS_FAILED'; // 住所の取得が失敗した。

// Initial State
export const initialState = {
  apiIsProcessing: false, // true はAPIのリクエストが投げられ、かつレスポンスがまだ返ってきていない状態
  zipCode: null,  // 入力された郵便番号
  address: null,  // APIが成功で返ってきた場合の該当住所
  error: null,    // APIが失敗で返ってきた場合のエラーメッセージ
};

// Reducer (exported as default)
export default function address(state = initialState, action) {
  switch(action.type) {
    case GET_ADDRESS_REQUESTED:
      return Object.assign({}, state,
        { apiIsProcessing: true, zipCode: action.payload.zipCode, address: null, error: null  });
    case GET_ADDRESS_SUCCEEDED:
      return Object.assign({}, state,
        { apiIsProcessing: false, address: action.payload.address });
    case GET_ADDRESS_FAILED:
      return Object.assign({}, state,
        { apiIsProcessing: false, error: action.payload.message });
    default:
      return state;
  }
}

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

以下、上記のコードについての補足です。

- アクションの書き方は、Ducks の Reules のひとつ、

> 3.MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE

　　に合わせています。

- アクションオブジェクトに payload というプロパティを持たせて、これの値として種々のパラメータを指定するのがFSAの流儀です。以下に説明があります。
    - https://github.com/redux-utilities/flux-standard-action#payload

　🐾**ここまでの追加・修正コミット： [Redux構成要素を追加](https://github.com/jun68ykt/saga-and-router4/commit/cd99b0027c03c80ce7137296384c37c252cbdc9f)**



# 5. ReactコンポーネントとRedux構成要素との連携

　APIの結果に応じた画面遷移は後で実装することにして、まずは、以下のユースケース

1. フォームから郵便番号を入力し、
2. 送信ボタンをクリックすると、入力された郵便番号を含む`GET_ADDRESS_REQUESTED` アクションが送出され、
3. APIの結果に応じて、以下のアクションが送出され、
  - 成功の場合には `GET_ADDRESS_SUCCEEDED`
  - 失敗の場合には `GET_ADDRESS_FAILED`
4. 上記のアクションにより、stateが期待どおり変更される。

をブラウザの開発ツールで確認できるところまでを実装します。

## 5.1 入力フォームに郵便番号の入力部分と送信ボタンを追加

`InputForm` に郵便番号の入力部分と送信ボタンを追加します。

`src/containers/form/InputForm.jsx`

```jsx
import React, { Component } from 'react';

class InputForm extends Component {
  constructor(props) {
    super(props);
    this.state = { 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();
    console.log(`郵便番号 ${this.state.zipCode} の住所取得を要求`); // TODO: dipatch で GET_ADDRESS_REQUESTEDを送出
  }

  render() {
    return(
      <div className="form">
        <form onSubmit={this.handleSubmit}>
          <input
            type="text"
            name="zipCode"
            value={this.state.zipCode}
            placeholder="半角数字で入力"
            onChange={this.handleChange} />
          <input type="submit" value="送信" />
        </form>
      </div>
    );
  }
}

export default InputForm;
```

上記で `handleChange(e) ` の中で、単に

```javascript
this.setState({ zipCode: e.target.value });
```

　とせずに、

```javascript
this.setState({ zipCode: e.target.value.replace(/[^0-9]/, '') });
```
としています。これによって、半角数字だけを受けつけるようにしています。


## 5.2 スタイル追加

見栄え良くするために、スタイルも適宜追加しておきます。

`public/css/form/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;
}
```


　以下の画面キャプチャは、上記の修正後、フォームに郵便番号を入力し、送信ボタンをクリックしたところです。Console に入力された郵便番号を含むデバッグログが表示されています。

<img width="860" alt="スクリーンショット 2018-05-13 13.14.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/2afebded-e13a-5ca6-e259-a453f50ebf95.png">

　🐾**ここまでの追加・修正コミット： [テキスト入力、送信ボタン、テキスト変更とサブミットハンドラ追加](https://github.com/jun68ykt/saga-and-router4/commit/555f1f070755713268bdcc3b5a086c7c0dd1b372)**

## 5.3 送信ボタンクリックでReduxアクションが送出されるようにする。

　Reactコンポーネントと Redux との連携を組み込んでいきます。redux、react-redux のほか、 アクションによるstate の内容をConsoleに表示させるため、redux-logger も追加します。

```shell-session
$ yarn add redux
$ yarn add react-redux
$ yarn add redux-logger
```

package.json に以下が追加されました。

```
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
```

上記のモジュールを追加したら、`src/index.js` でRedux storeを作成して、各画面のコンテナに渡すように修正します。

`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 { InputForm, Success, Failure } from './containers';
import address, { initialState } from "./ducks/address";

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

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'));
```

　次に、フォーム画面のコンテナにreduxのstateとdispachが渡るようにし、`handleSubmit(e)`　で
`GET_ADDRESS_REQUESTED`アクションを送出するように修正します。

`src/containers/form/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.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();
    this.props.dispatch(getAddressRequested(this.state.zipCode));
  }

  render() {
    return(
      <div className="form">
        <form onSubmit={this.handleSubmit}>
          <input
            type="text"
            name="zipCode"
            value={this.state.zipCode}
            placeholder="半角数字で入力"
            onChange={this.handleChange} />
          <input type="submit" value="送信" />
        </form>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return state
}

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

　上記の修正後、フォーム画面から郵便番号を入力して、送信ボタンをクリックすると以下のように、`GET_ADDRESS_REQUESTED` が発行され、stateが変更されることを確認できます。

<img width="1258" alt="スクリーンショット 2018-05-13 22.47.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/74659/9bb12089-e04e-3a07-a13f-50202d530dff.png">

　🐾**ここまでの追加・修正コミット： [送信ボタンクリックでアクションが送出されるように修正](https://github.com/jun68ykt/saga-and-router4/commit/48060413b7002c2b164b361da012da2edc9fcdc4)**

# 後編へ

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