19
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 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初心者〜中級のフロントエンド開発者を対象読者として想定
  • React初心者にも分かりやすいように、スクラッチから段階をふんでコードを追加していきます。
    • ただし、ここでいう「React初心者」とは、Javascript初心者を意味していません。特に redux-saga を使いこなすには、ES6から導入されたGenerator の理解が必須というのが私の考えです。
  • コード追加、修正を行ったGitコミットを各段階の終わりに示します。
  • React中〜上級者の方へ: 本投稿の主題(最も関心のある課題)の解決をしているのは、後編の以下のセクションです。

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

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

  • 最終的に出来上がる画面と機能は、8.4 動作確認 の画面キャプチャのとおりです。

  • 仕様の概要は以下です。

  1. 画面構成
  • 以下の2つの画面から構成される。
    • 入力フォーム画面: 郵便番号を入力するテキスト入力と送信ボタンを持つフォーム
    • 結果表示画面:入力フォームで入力された郵便番号で住所を検索した結果を表示
  1. 各画面のURL
  • 入力フォーム画面: /
  • 結果表示画面:
    • 成功の場合: /success
    • 失敗の場合: /failure
  1. 利用するAPI

利用するNodeモジュール

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

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

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

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

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 のバージョンは以下です。
(以下で $ はシェルのプロンプトです。)

$ 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 して、ブラウザにデフォルト画面が表示されることを確認します。

$ 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 でアプリを終了させます。


(・・・ 略)


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

^C
$ 

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

$ 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 を以下のように修正します。

<!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 を以下のように空のファイルとして作成しておきます。

$ mkdir public/css
$ > public/css/style.css
$ cat public/css/style.css
$

 
 この状態で、 yarn start して、ブラウザの開発ツールのConsole にエラーが表示されることなく、白紙のページが表示されることを確認できたら準備完了です。

 🐾ここまでの追加・修正コミット: 最初のコミット

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

2.1 react-router-domを追加

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

$ yarn add react-router-dom

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

"react-router-dom": "^4.2.2",

2.2 各画面コンポーネント

 
 各画面に対応するコンポーネントを作成します。
 これらは、Presentational and Container Components で説明されているContainer Componentsになるものなので、src/ の下に containers/ フォルダを作り、ここに以下の3点を作成します。

2.2.1 入力フォーム:

src/containers/forms/InputForm.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

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

import React, { Component } from 'react';

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

export default Failure;

2.2.4 src/containers/index.js

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

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 を以下のように書きます。

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 すると、以下の画面キャプチャ

スクリーンショット 2018-05-13 21.01.46.png スクリーンショット 2018-05-13 21.02.09.png スクリーンショット 2018-05-13 21.02.38.png

のように、

ことが確認できます。

 🐾ここまでの追加・修正コミット: 各画面のコンテナを作成し、ルーティングを設定

3. 郵便番号API

 本稿で作るWebフロントの仕様に記載したように、郵便番号による住所検索サービスとして、PostCodeJP様提供のWebAPIを利用させて頂きます。

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

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

src/api/postcode-jp.js

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追加

4. Redux構成要素の実装

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

 上記のDucks: Redux Reducer Bundlesサンプルにならい、src/ducksというフォルダを作り、ここにaddress.jsというファイルを作成して、この一つのファイルにアクション、リデューサ、アクションクリエータを実装します。

src/ducks/address.js

// 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

  に合わせています。

 🐾ここまでの追加・修正コミット: Redux構成要素を追加

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

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

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

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

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

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

src/containers/form/InputForm.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) の中で、単に

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

 とせずに、

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

としています。これによって、半角数字だけを受けつけるようにしています。

5.2 スタイル追加

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

public/css/form/style.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 に入力された郵便番号を含むデバッグログが表示されています。

スクリーンショット 2018-05-13 13.14.19.png

 🐾ここまでの追加・修正コミット: テキスト入力、送信ボタン、テキスト変更とサブミットハンドラ追加

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

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

$ 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

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

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が変更されることを確認できます。

スクリーンショット 2018-05-13 22.47.26.png

 🐾ここまでの追加・修正コミット: 送信ボタンクリックでアクションが送出されるように修正

後編へ

 後編に続きます
👉 Reactサンプル:redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させる。(後編)

19
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?