(もはやタイトルがわけわかんないですね。まぁSEO的にも無難が一番かなと)
こちらの記事の続編です。
http://qiita.com/uryyyyyyy/items/41334a810f1501ece87d
※編集履歴
- react-router v4に対応
- React16-beta & TS2.4 対応
問題提起
今時、型チェックのない言語とか使いたくないですよね!(5回目)
今回はreact-routerを導入して、ついでに複数reducer対応もやってみます。
環境
- NodeJS 8.2
- React 16.0-beta
- TypeScript 2.4
- webpack
- react-router 4.1
構成
こちらをご参照ください。
https://github.com/uryyyyyyy/react-redux-sample/tree/react-router
react-router導入の前に
今時のSPAだと、URLは http://localhost/todo/1
みたいな感じのURLになるのが一般的ですね。
普通に組んでしまうと、/todo/1へアクセス来たときにはtodo/1の階層にあるファイルを取得してきてしまい、SPAではなくなってしまいます。
そうではなくて、いつでもrootのindex.htmlを返すようにサーバー側で設定でする必要があります。
これまでの記事で既にexpressを導入していたのですが、expressではこのように記述します。
const path = require('path');
const express = require('express');
const app = express();
app.use('/dist', express.static('dist'));
app.get('/api/count', (req, res) => {
res.contentType('application/json');
const obj = {"amount": 100};
setTimeout(() => res.json(obj), 500);
//res.status(400).json(obj); //for error testing
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(3000, (err) => {
if (err) {
console.log(err);
}
console.log("server start at port 3000")
});
expressの書き方はここでの主題ではないのですが簡単に書くと、
- /distの時はdistで返す。
- バンドルされたjsやcssを返す
- /api/countのときはjsonを作って返す。
- それ以外のURLは全部rootのindex.htmlにリダイレクトする。
となります。
これで、いきなり /todo/1
などのページに飛んでも、
該当ページにマッチするURLがない→rootのindex.htmlを読む→dist以下のjsが読まれる→jsのreact-routerが正しいページを表示する、という流れになり、求めていた形になります。
(※全部index.htmlに返すのが良いのかとか疑問がありますが、react-routerのissueでも、railsのSPA対応などでも同じようにしているのでそういうものらしいです。)
(※ちなみに、こうした場合、client側ではどこにアクセスしてもindex.htmlが返ってくるので、他リソースへのアクセスは絶対パスで書く必要があります。
なぜなら、例えば /todo/1
とかでアクセスした場合、それで取れたindex.htmlに相対パスで書かれていると、 /todo
の階層の静的ファイルを探しに行ってしまうからです。
React-routerの導入
Index.tsx
まずindex.tsxをこう変えます。
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Router } from 'react-router';
import store from "./store";
import {Provider} from "react-redux";
import createBrowserHistory from 'history/createBrowserHistory';
import {Routes} from "./Routes";
const history = createBrowserHistory()
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Routes />
</Router>
</Provider>
, document.getElementById('app')
);
<Provider store={store}>
はreact-reduxの仕組みなので割愛するとして、 <Router>
の中でhistory(対象がブラウザなので、それ用のオブジェクト)を入れることで準備ができます。
ちなみに、ここでブラウザ用でなくテスト用やNative用のhistoryオブジェクトを入れることで環境毎に使い分けることが出来るようになっています。
ここでのRoutesクラスは自前で定義したものなので、次はコレを見てみます。
Routes.tsx
import * as React from 'react';
import { Switch } from 'react-router';
import {Link, Route} from 'react-router-dom';
import Counter from './counter/Container';
import NotFound from './NotFound';
export class Routes extends React.Component<{}, {}> {
render() {
return (
<div>
<h1>React Redux sample</h1>
<li><Link to='/' >Home</Link></li>
<li><Link to='/counter' >Counter</Link></li>
<li><Link to='/counter/papaparam' >Counter with param</Link></li>
<Switch>
<Route exact path='/counter' component={Counter} />
<Route path='/counter/:myParams' component={Counter} />
<Route component={NotFound}/>
</Switch>
</div>
)
}
}
上半分のli要素などは、どのページにいても各ページへ飛べるようにリンクを並べています。
SPAの場合は普通のaタグなどでページ遷移してしまうとHTMLを取得し直すため画面が一瞬真っ白になってしまったり、滑らかな遷移ができません。そこでrouterに付いているLinkコンポーネントで画面遷移をすることが推奨されます。
次にSwitchコンポーネントです。
ここではURLに応じて表示するコンポーネントを切り替えることが出来ます。
例えば /counter
であればCounterコンポーネントが表示される、といった具合です。
(この例では、 /
に来るとNotFoundのページが表示されるようになっていますね。。)
Container.tsx
import {Counter} from './Counter'
import {connect, MapDispatchToPropsParam, MapStateToPropsParam} from 'react-redux'
import {Dispatch} from 'redux'
import {CounterState, decrementAmount, fetchRequestFinish, fetchRequestStart, incrementAmount} from './module'
import {ReduxAction, ReduxState} from '../store'
import {RouteComponentProps} from 'react-router'
const mapStateToProps: MapStateToPropsParam<{value: CounterState, param?: string}, any> = (state: ReduxState, ownProps: RouteComponentProps<{myParams: string | undefined}>) => {
if (ownProps.match.params.myParams === undefined) {
return {value: state.counter}
}
return {value: state.counter, param: ownProps.match.params.myParams}
}
const mapDispatchToProps: MapDispatchToPropsParam<{actions: ActionDispatcher}, {}> = (dispatch: Dispatch<ReduxAction>) => ({actions: new ActionDispatcher(dispatch)})
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
こちらは必要以上に型情報を書き込んでいるので、少々読みにくいかもしれないですね。。
react-routerでクエリパラメータなどをやりとりたい場合には、reduxの外(OwnProps)から渡されることになります。
mapStateToProps
の方に記載されているOwnPropsがそれで、 RouteComponentProps
型で内部にmyParams(routerの方で指定した変数名)が入ってくるので、 ownProps.match.params.myParams
という形でmyParamsを取得してコンポーネントのpropsに渡すようにしています。 {value: CounterState, param?: string}
の部分ですね。
こうすることで、Counterコンポーネント側ではparamという変数が渡ってくることが型のチェックで保証されることになります。
テストについて
routerのコンポーネント(LinkやSwitchなど)を含むコンポーネントを単体テストしたい時は、そのまま描画してしまうと <Router>
でinjectされたhistoryオブジェクトが存在しないためエラーになってしまいます。
その場合は、MemoryRouterというものでテスト対象のコンポーネントを囲ってあげるとテストができます。
import { MemoryRouter } from 'react-router'
<MemoryRouter>
<YourComponent />
</MemoryRouter>
まとめ
react-router v4を入れてみました。
experimentalとしてredux用のライブラリもありますが、このように使えば何も問題ないかと思います。
次回はこれをベースに色々なライブラリの実装サンプルを公開していこうと思います。