28
30

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-06

はじめに

通常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で管理する

必要なモジュールのインストール

基本的には最新版で書いていきます。

# 環境っぽいもの
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かただの文字列かを決定するだけの共通表示コンポーネントです。
根っこのコンポーネントではConnectedRoutercreateBrowserHistoryで作ったhistoryをぶちこんで、routerMiddlewareを適用します。
reduxを扱う部分では、reducerstateにRouter用の領域を設けるだけです。

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);
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);
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を並べてるだけです。

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

ReactDOM.render(
    <App/>,
    document.getElementById('root'));
pages/Home.tsx
import * as React from 'react';

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

export default component;
pages/Foo.tsx
import * as React from 'react';

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

export default component;
pages/Bar.tsx
import * as React from 'react';

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

export default component;
index.html
<!DOCTYPE html>
<html>
<head>
    <title>Router sample</title>
    <meta charset="utf-8">
</head>
<body>
<div id="root"></div>
</body>
</html>

完成したが…?

これでビルドすれば完成です。

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をそのように書き換えます。

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

28
30
0

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
28
30