Reactとreact-routerとconnected-react-routerとTypeScript

## はじめに
通常Webサイトを作るときは複数ページ構成になることが多いと思います。
Reactを使って、複数ページを行き来する場合はreact-routerなるものを使うのが一番近道だと思います。
例によってフロントを始めた時に、こんなドキュメントがあればハマらなかっただろうな…と思うことの備忘です。
いつものように、よくわからなければ**公式ドキュメントとエラーメッセージをよく読むのが解決への近道**です。

## 前提条件
https://qiita.com/IgnorantCoder/items/88f13569cbf0a1c5eaa1
が終わってる。つまりreduxとreact-reduxは使ったことがある前提です。

## そもそもreact-routerとは
React routerとはなにかですが、公式を見に行けば説明が書いてあります。

> Components are the heart of React's powerful, declarative programming model. React Router is a collection of navigational components that compose declaratively with your application. Whether you want to have bookmarkable URLs for your web app or a composable way to navigate in React Native, React Router works wherever React is rendering--so take your pick!
>
> React Router: Declarative Routing for React.js (https://reacttraining.com/react-router/)

英語は嫌いなので、日本語に直すと、

コンポーネントこそがReactの強力な宣言的プログラミングの本質です。React Routerとは宣言的なナビゲーションコンポーネントを集めたものです。ウェブアプリとしてブックマーク可能なURLを持ちたいときや、React Nativeでナビゲーションする場合でも、Reactでレンダリングされてる限りReact Routerは動きます、これでやっとけ！

要するにこれを使えばいい感じでReactでつくったページたちをURLでルーティングできますよ的なことですね。

## とりあえず作ってみる
今回以下の構成で作ってみます。

- /と/fooと/barの3ページ
- すべてのページに全ページへのリンクが張ってあるが、今表示中のページだけリンクになってない
- ページの状態をreduxで管理する

### 必要なモジュールのインストール
基本的には最新版で書いていきます。

```shell
# 環境っぽいもの
npm install --save-dev html-webpack-plugin ts-loader tslint-config-airbnb tslint-loader typescript webpack webpack-cli webpack-serve connect-history-api-fallback koa-connect

# 今回使うライブラリ全部
npm install --save react react-dom react-redux react-router react-router-dom connected-react-router redux

# と、その型定義ファイル
npm install --save-dev @types/react @types/react-dom @types/react-redux @types/react-router @types/react-router-dom 
```

### いつもの環境設定ファイル
webpack.config.jsとtsconfig.jsonとtslint.jsonですね。
webpackの設定ファイルでは、今回はwebpack-serveが立ち上がるように記述しておきます。
ただ、実はwebpackの設定ファイルは、このままでは問題があるので、あとで修正します。

```webpack.config.js
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.tsx',
    devtool: 'inline-source-map',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                enforce: 'pre',
                loader: 'tslint-loader',
                exclude: [
                    /node_modules/
                ],
                options: {
                    emitErrors: true,
                }
            },
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: [
                    /node_modules/
                ],
                options: {
                    configFile: 'tsconfig.json',
                }
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: 'static/js/bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    server: {
        content: path.resolve(__dirname, 'dist'),
        port: 3000,
    },
    plugins: [
        new htmlWebpackPlugin({
            template: "index.html"
        })
    ]
};
```

```tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "lib": ["ES2018", "dom"],
    "moduleResolution": "node",
    "removeComments": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "strictFunctionTypes": false
  }
}
```

```tslint.json
{
  "extends": "tslint-config-airbnb",
  "rules": {
    "no-boolean-literal-compare": false,
    "ter-indent": [true, 4]
  }
}
```

### 一筆書きしてみる
サンプルコードは筆書きしたら一発でビルドも通る程度でしたので、大した内容ではありません。
ナビゲーションバーは、Routerの状態から現在の`path`をとって`Link`かただの文字列かを決定するだけの共通表示コンポーネントです。
根っこのコンポーネントでは`ConnectedRouter`に`createBrowserHistory`で作った`history`をぶちこんで、`routerMiddleware`を適用します。
reduxを扱う部分では、`reducer`と`state`にRouter用の領域を設けるだけです。

```tsx:containers/NavBar.tsx
import * as React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { RootState } from '../modules';

type OwnProps = { // NavBarに実際表示する情報は外から与える
    data: {
        display: string;
        path: string;
    }[];
};

const mapStateToProps = (state: RootState, ownProps: OwnProps) => {
    return {
        items: ownProps.data.map((e) => {
            return {
                display: e.display,
                to: e.path,
                disabled: state.router.location == null
                    ? false
                    : state.router.location.pathname === e.path, // stateからrouterの状態がとれる
            };
        }),
    };
};

type Props = {
    items: {
        display: string;
        to: string;
        disabled: boolean;
    }[];
};

const component: React.SFC<Props> = (props: Props) => {
    return (
        <ul>
            {
                props.items.map((e) => {
                    return (
                        <li key={e.display}>
                            {
                                e.disabled
                                    ? e.display
                                    : <Link to={e.to}>{e.display}</Link>
                            }
                        </li>
                    );
                })
            }
        </ul>
    );
};

export default connect(mapStateToProps)(component);
```

```tsx:modules/index.tsx
import { RouterState } from 'connected-react-router';
import { combineReducers } from 'redux';

export type RootState = {  // 必要なら当然自分用のstateも追加してOK
    router: RouterState,   // Router用の領域
};

export const rootReducer = combineReducers({ // 必要に応じて自分用のreducer追加
} as any);
```

```tsx:App.tsx
import * as React from 'react';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router';
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
import createBrowserHistory from 'history/createBrowserHistory';
import { rootReducer } from './modules';
import Home from './pages/Home';
import Foo from './pages/Foo';
import Bar from './pages/Bar';
import NavBar from './containers/NavBar';

const history = createBrowserHistory();           // Browser historyをとって
const store = createStore(
    connectRouter(history)(rootReducer),
    applyMiddleware(routerMiddleware(history)));  // router用のmiddlewareを適用しておく

const component: React.SFC = () => {
    // ConnectedRouterは1エレメントしかとれないのでRoute群はdivで囲っておく
    // Switchで囲っておくと呼ばれたやつだけrenderingする
    // 共通コンポーネントはSwitchの外側に置いとけば良い

    return (
        <Provider store={store}>
            <ConnectedRouter history={history}>
                <div>
                    <NavBar
                        data={[
                            { display: 'to home', path: '/' },
                            { display: 'to foo', path: '/foo' },
                            { display: 'to bar', path: '/bar' },
                        ]}
                    />        
                    <Switch>
                        <Route exact path={'/'} component={Home}/>
                        <Route exact path={'/foo'} component={Foo}/>
                        <Route exact path={'/bar'} component={Bar}/>
                    </Switch>
                </div>
            </ConnectedRouter>
        </Provider>
    );
};

export default component;
```

その他に追加したソースはシンプルにComponentを並べてるだけです。

```tsx:index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
    <App/>,
    document.getElementById('root'));
```

```tsx:pages/Home.tsx
import * as React from 'react';

const component: React.SFC = () => {
    return (
        <div>
            This is Home.
        </div>
    );
};

export default component;
```

```tsx:pages/Foo.tsx
import * as React from 'react';

const component: React.SFC = () => {
    return (
        <div>
            This is Foo.
        </div>
    );
};

export default component;
```

```tsx:pages/Bar.tsx
import * as React from 'react';

const component: React.SFC = () => {
    return (
        <div>
            This is Bar.
        </div>
    );
};

export default component;
```

```HTML:index.html
<!DOCTYPE html>
<html>
<head>
    <title>Router sample</title>
    <meta charset="utf-8">
</head>
<body>
<div id="root"></div>
</body>
</html>
```

## 完成したが…？
これでビルドすれば完成です。

```shell
webpack-serve --config webpack.config.js
```

で立ち上がります。リンクをクリックしてもうまくページが遷移できると思います。
しかし、アドレスバーに直接 http://localhost:3000/foo などを入力すると`Cannnot GET /foo`と表示されて、うまくいかないはずです。
これは考えてみると当たり前で、 http://localhost:3000/ にアクセスするとhttp://localhost:3000/index.html が読み込まれ、その中でロードしているbundle.jsがレンダリングというなか、いきなり http://localhost:3000/foo に飛ばれた場合は http://localhost:3000/ にフォールバックしてあげる必要があるわけです。
そこでwebpack.config.jsをそのように書き換えます。

```diff:webpack.config.js
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const history = require('connect-history-api-fallback');
const convert = require('koa-connect');

module.exports = {
    mode: 'development',
    entry: './src/index.tsx',
    devtool: 'inline-source-map',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                enforce: 'pre',
                loader: 'tslint-loader',
                exclude: [
                    /node_modules/
                ],
                options: {
                    emitErrors: true,
                }
            },
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: [
                    /node_modules/
                ],
                options: {
                    configFile: 'tsconfig.json',
                }
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: 'static/js/bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    serve: {
        content: path.resolve(__dirname, 'dist'),
        port: 3000,
        add: (app, middleware, options) => {
            app.use(convert(history({ index: '/' })));
        }
    }
    plugins: [
        new htmlWebpackPlugin({
            template: "index.html"
        })
    ]
};
```
これで、OKです。
これはAWSなどにデプロイする場合も同じで、404なときにindex.htmlを返してあげるような設定をしましょう。

できあがったソース群はこちら
https://github.com/IgnorantCoder/typescript-react-redux-router-sample

## まとめ
Reduxさえ理解していればreact-router自体は記述量も少なく、難しいこともあまりないため導入はとても簡単ですが、バージョンアップの度にインターフェースが破壊的に変わるようで、調べると古い記述も多いです。(この記事も正しいかやや不安)
ただ、現状これ以外に有効な選択肢もなさそうですので、とりあえずバージョンを固定して導入してみるのが良いと思います。