Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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 の冒頭に以下を追加します。

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

GET_ADDRESS_REQUESTEDアクションを受け取ったら、3.1 郵便番号から住所を取得 で用意しておいたAPIを呼び、その結果が

  • 成功なら GET_ADDRESS_SUCCEEDED
  • 失敗なら GET_ADDRESS_FAILED

を発生させるsagaを以下のように実装して、これを src/ducks/address.js に追加します。

// 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 に追加します。

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

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

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

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

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

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

 🐾ここまでの追加・修正コミット: SagaからAPIをリクエストして結果に応じたアクションを送出

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

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

7.1 参考にした GitHub issue

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

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

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

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

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

FSAの説明に、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 を追加して以下のようにします。

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

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

  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 のコード例:

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 で、saga に routerを渡しているコードを参考にします。

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

$ yarn add history

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

"history": "^4.7.2",

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

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 します。

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

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

// 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 成功時

スクリーンショット 2018-05-13 15.55.31.png
 上記で、送信をクリックすると、以下のように /success に遷移して、 Successコンテナが renderされる。

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

7.4.2 失敗時

スクリーンショット 2018-05-13 16.00.40.png
上記で、送信をクリックすると、以下のように /failure に遷移して、 Failureコンテナが renderされる。

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

 🐾ここまでの追加・修正コミット: sagaとhistoryによる画面遷移

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

8. 画面表示の改良

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

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

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

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

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

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

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

8.1 結果表示画面

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

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

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

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

8.3 スタイルシート

最後に、public/css/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;
}

.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; }

 🐾ここまでの追加・修正コミット: 画面表示の改良

8.4 動作確認

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

8.4.1 成功時

スクリーンショット 2018-05-14 0.45.58.png

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

スクリーンショット 2018-05-14 0.46.08.png

8.4.2 失敗時

スクリーンショット 2018-05-14 0.46.36.png

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

スクリーンショット 2018-05-14 0.46.43.png

おわりに

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

 👉前編に戻る

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした