更新情報
-
2020/04/12: 使用するNodeモジュールを最新にし、また住所検索APIの仕様変更に追従しました。
-
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の結果に応じて画面遷移させる。(前編)
- Reactサンプル:redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させる。(後編)
対象読者
- React初心者〜中級のフロントエンド開発者を対象読者として想定
- React初心者にも分かりやすいように、スクラッチから段階をふんでコードを追加していきます。
- ただし、ここでいう「React初心者」とは、Javascript初心者を意味していません。特に redux-saga を使いこなすには、ES6から導入されたGenerator の理解が必須というのが私の考えです。
- コード追加、修正を行ったGitコミットを各段階の終わりに示します。
- React中〜上級者の方へ: 本投稿の主題(最も関心のある課題)の解決をしているのは、後編の以下のセクションです。
本稿で作成するWebフロントの仕様
-
入力された郵便番号から住所を検索して表示します。
-
最終的に出来上がる画面と機能は、8.4 動作確認 の画面キャプチャのとおりです。
-
仕様の概要は以下です。
- 画面構成
- 以下の2つの画面から構成される。
- 入力フォーム画面: 郵便番号を入力するテキスト入力と送信ボタンを持つフォーム
- 結果表示画面:入力フォームで入力された郵便番号で住所を検索した結果を表示
- 各画面のURL
- 入力フォーム画面: /
- 結果表示画面:
- 成功の場合: /success
- 失敗の場合: /failure
- 利用するAPI
- 郵便番号による住所検索サービスとして、PostCodeJP様提供のWebAPIを利用させて頂きます。
利用するNodeモジュール
以下のモジュールを利用します。
- react および以下のモジュール
- react-dom
- react-redux
- react-router-dom
- redux および以下のmidllewareモジュール
- redux-logger
- redux-saga
- history
参照するブログ記事、GitHubレポジトリなど
本稿では、以下の記事、GitHubレポジトリのREADME、issue等を、適宜参照します。
- Presentational and Container Components
- Flux Standard Action
- Ducks: Redux Reducer Bundles
- Using v4 with redux-saga
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
すると、以下の画面キャプチャ
のように、
- 初期画面( http://localhost:3000/ )では InputForm と表示され、
- アドレスバーに、 http://localhost:3000/success と入力すると、Success と表示され、
- http://localhost:3000/failure と入力すると、画面に Failure と表示される
ことが確認できます。
🐾ここまでの追加・修正コミット: 各画面のコンテナを作成し、ルーティングを設定
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
に合わせています。
- アクションオブジェクトに payload というプロパティを持たせて、これの値として種々のパラメータを指定するのがFSAの流儀です。以下に説明があります。
🐾ここまでの追加・修正コミット: Redux構成要素を追加
5. ReactコンポーネントとRedux構成要素との連携
APIの結果に応じた画面遷移は後で実装することにして、まずは、以下のユースケース
- フォームから郵便番号を入力し、
- 送信ボタンをクリックすると、入力された郵便番号を含む
GET_ADDRESS_REQUESTED
アクションが送出され、 - APIの結果に応じて、以下のアクションが送出され、
- 成功の場合には
GET_ADDRESS_SUCCEEDED
- 失敗の場合には
GET_ADDRESS_FAILED
- 上記のアクションにより、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 に入力された郵便番号を含むデバッグログが表示されています。
🐾ここまでの追加・修正コミット: テキスト入力、送信ボタン、テキスト変更とサブミットハンドラ追加
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が変更されることを確認できます。
🐾ここまでの追加・修正コミット: 送信ボタンクリックでアクションが送出されるように修正
後編へ
後編に続きます
👉 Reactサンプル:redux-sagaとreact-router v4でWebAPIの結果に応じて画面遷移させる。(後編)