React v16 + react-router v4 + ExpressでSSR+SPAする

## 経緯
React v16.3が出てサーバサイドレンダリング（以下SSR）は`renderToNodeStream`と`hydrate`が追加されました。
同じくreact-routerもv4になり、シンタックス含め大きく変更されました。
今回はその2つを使用しExpressでサーバーを立てて、isomorphicらしいSSR + SPA環境を学びます。

## ツール

### React v15 -> v16.3.0
React v16.3.0のSSRについては以下の記事などでまとめられています。
ここでも参考までに少し書いておきます。

> React v16でのサーバーサイドレンダリング - blog.koba04.com
> http://blog.koba04.com/post/2017/10/01/serverside-rendering-in-react-v16/

#### v15

v15までは`renderToStatic`と`render`を使い、サーバーサイドでは`dangerouslySetInnerHTML`を使ってStringとしてクライアントサイドに返す必要がありました。
HTML部分も`renderStaticMarkUp`でアプリケーション部分は`renderToString`でとちょっと複雑な部分もあったのです。

```jsx:Html.js
import React from 'react';

const Html = (props) => {
    return (
        <html>
            <head>
                <title>App</title>
            </head>
            <body>
                <div id="app" dangerouslySetInnerHTML={ {__html: props.markup} }></div>
                <script src="/static/bundle.js"></script>
            </body>
        </html>
    );
};

export default Html;
```

```jsx:server.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';

import Html from './Html';
import App from './App';

const app = express();

app.get('/', (req, res) => {
    res.send(
        ReactDOMServer.renderToStaticMarkup(
            <Html
                markup={ReactDOMServer.renderToString(<App />)}
            />
        )
    );
});

app.listen(3000);
```
#### v16.3.0
v16以降ではNode Streamに対応した`renderToNodeStream`やクライアントサイドで実行される`hydrate`というAPIが登場し、サーバーサイドとクライアントサイドでエントリーポイントを合わせる必要がなくなりました。
そのためv15のときのような、ComponentをStringに変換する必要もありません。

```jsx:Html.js
import React from 'react';

const Html = (props) => {
    return (
        <html>
            <head>
                <title>App</title>
            </head>
            <body>
                <div id="app">{props.children}</div>
                <script src="/static/bundle.js"></script>
            </body>
        </html>
    );
};

export default Html;
```

```jsx:server.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';

import Html from './Html';
import App from './App';

const app = express();

app.get('/', (req, res) => {
    ReactDOMServer.renderToNodeStream(
        <Html>
            <App />
        </Html>
    ).pipe(res);
});

app.listen(3000);
```

こうして見ると大分コードがシンプルになっていることがわかります。
今回はこちらを使用します。

### react-router v3 -> v4
react-routerもv4ではv2及びv3から破壊的な変更が行われました。
内容については以下の記事などでまとめられています。

> サンプルでreact-router v4を理解してみよう。 - Qiita
> https://qiita.com/park-jh/items/b4c7b16ea9eb0cf44942#onenter%E3%81%AE%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%ABcomponentwillmount%E5%8F%88%E3%81%AFcomponentwillreceiveprops%E3%82%92%E4%BD%BF%E3%81%86

ブラウザ側では`BrowserRouter`を使用し、サーバーでは`StaticRouter`を使うようにします。
あとでサンプルコードを書きますので追って説明します。

今回は`react-router-config`を使用するためそこまで影響ないのですが、
以下のところは気をつけました。

> reactjs/react-router-reduxを使っていたかもしれない。
> しかし、このライブラリはもうメンテナンスされない。
> react-router-reduxのメンテナンスはreact-training organizationに移管された。
> これからはreact router v4を使う。

なので今回はreact-router-reduxはなしでいきます！

## react-router-config
上述した`react-router`のヘルパーを担うプラグインです。
現在アルファ版になっているのでどうなるかはちょっと未定ではありますが、便利なので使う感じです。
> https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config

たとえばこんな感じでルーティング用の配列を用意して、

```js:routes.js
import Root from './Root';
import Home from './Home';
import About from './About';

const routes = [
  { component: Root,
    routes: [
      { 
        path: '/',
        exact: true,
        component: Home
      },
      { 
        path: '/about',
        exact: true,
        component: About
      }
    ]
  }
];

export default routes;
```

後は`renderRoutes`で使うだけです。

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';

import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import Routes from './routes';

ReactDOM.hydrate(
    <BrowserRouter>
        {renderRoutes(Routes)}
    </BrowserRouter>,
    document.getElementById('app')
);
```

コードがシンプルになるためこちらを採用します。
ちなみに`index.js`で使用している`hydrate`はSSRされたコンポーネントを自動的に使いまわすことでリソース削減を自動で行ってくれる便利な関数です。SSRするなら`render`ではなく`hydrate`を使用してよいでしょう。

## 環境
本題に入ります。
筆者の環境は以下になります。

|Name|Version|
|:---|:---|
|Node.js|8.9.0|
|React|16.3.0|
|react-router-config|1.0.0-beta.4|
|react-router-dom|4.2.2|
|Redux|4.0.0|
|Express|4.16.3|

## ディレクトリ
ディレクトリ構成は以下のようになります。
Actionsはとりあえずデモのため空にしています。

```
public/
    - client.js
    - server.js
src/
    - Actions
    - Container
        - About.js
        - App.js
        - Html.js 
    - Routes
        - routes.js
        - serverRoutes.js 
    - Store
        - Reducers 
    - client.js
    - server.js
.babelrc
package.json
webpack.config.js
```

## パッケージのインストール
必要なパッケージをインストールしていきます。

```
$ yarn init
$ yarn add express react react-dom react-redux react-router-config react-router-dom redux -S
$ yarn add babel-core babel-loader babel-preset-env babel-preset-react webpack  webpack-cli -D
```

## 設定ファイル
設定ファイルの中身を先に記載します。
どちらもデモなのでシンプルにしています。

```js:.babelrc
{
  "presets": [
    "env", "react"
  ]
}
```

```js:webpack.config.js
const path = require('path');

const env = process.env.NODE_ENV;

const config = {
    mode: env || 'development',
    target: 'node',
    entry: {
        client: [path.resolve(__dirname, 'src/client.js')],
        server: [path.resolve(__dirname, 'src/server.js')],
    },
    output: {
        path: path.resolve(__dirname, './public/'),
        publicPath: './public/',
        filename: '[name].js',
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                loader: 'babel-loader',
            },
        ],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    }
};

module.exports = config;
```

```json:package.json
    "scripts": {
        "server": "node public/server.js",
        "build": "webpack -d --progress --colors --display-error-details"
    }
```


## 各ページ

### client.js

Reduxを使うため`Provider`経由で`store`を渡しています。
ブラウザ側で読み込むためreact-router-domからは`BrowserRouter`を使用します。

```jsx:client.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './Store/store';

import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import Routes from './Routes/routes';

const store = configureStore();

ReactDOM.hydrate(
    <Provider store={store}>
        <BrowserRouter>{renderRoutes(Routes)}</BrowserRouter>
    </Provider>,
    document.getElementById('app')
);
```

### Store

storeはおなじみな感じです。
ミドルウェアはサンプルでLoggerを入れてますが、追加する場合は`middlewares`にpushすればOKです。

#### store.js

```jsx:Store/store.js
import { createStore, applyMiddleware } from 'redux';
import Logger from 'redux-logger';

import reducer from './reducer';

const PRODUCTION = process.env.NODE_ENV === 'production';

const configureStore = () => {
    const middlewares = [];

    if (!PRODUCTION) {
        middlewares.push(Logger);
    }

    const store = createStore(reducer, applyMiddleware(...middlewares));

    return store;
};

export default configureStore;
```

#### Reducer.js

```jsx:Store/Reducer.js
import { combineReducers } from 'redux';

const Reducer = combineReducers({
    // Reducerを追加すればOK
});

export default Reducer;
```

### Routes

ルーティングは以下のようにしました。

#### routes.js

```jsx:Routes/routes.js
import Html from '../Container/Html';
import App from '../Container/App';
import About from '../Container/About';

const Routes = [
    {
        component: Html,
        routes: [
            {
                path: '/',
                exact: true,
                component: App,
            },
            {
                path: '/about',
                exact: true,
                component: About,
            },
        ],
    },
];

export default Routes;
```

#### ServerRoutes.js
サーバー用のルートは`express.Router`を使ってルータ用のインスタンスを作成します。
ここで`rendetToNodeStream`を使って、react-routerの`StaticRouter`を通したコンポーネントをNode Streamとして渡せるようにします。

```jsx:Routes/ServerRoutes.js
import express from 'express';

import React from 'react';
import ReactDOMServer from 'react-dom/server';

import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';

import routes from './routes';

const router = express.Router();

router.get('*', (req, res) => {
    let context = {};

    ReactDOMServer.renderToNodeStream(
        <StaticRouter location={req.url} context={context}>
            {renderRoutes(routes)}
        </StaticRouter>
    ).pipe(res);
});

module.exports = router;
```

### server.js

上のように書くとサーバーの処理は以下だけになりました。

```js:server.js
import express from 'express';

const app = express();
const router = require('./Routes/serverRoutes');

app.use(express.static('public'));

app.use('/', router);
app.use('/about', router);

app.listen(3000);
```

### Components

各Componentは以下のようになります
ポイントはRoot Componentである`Html.js`で`renderRoutes`を使い親から渡されるコンポーネントを描画していることです。

#### Html.js

```jsx:Html.js
import React from 'react';
import { renderRoutes } from 'react-router-config';

const Html = props => {
    return (
        <html>
            <head>
                <title>App</title>
            </head>
            <body>
                <div id="app">{renderRoutes(props.route.routes)}</div>
                <script src="/client.js" />
            </body>
        </html>
    );
};

export default Html;
```

#### App.js

```jsx:App.js
import React from 'react';
import { Link } from 'react-router-dom';

const App = props => {
    return (
        <div>
            Name is {props.name || 'hoge'} <br />
            Path is {props.match.path} <br />
            <Link to={'/about'}>about</Link>
        </div>
    );
};

export default App;
```

#### About.js

```jsx:About.js
import React from 'react';
import { Link } from 'react-router-dom';

const About = props => {
    return (
        <div>
            This Page is About Page!!<br />
            <Link to={'/'}>top</Link>
        </div>
    );
};

export default About;
```

## 動かしてみる

ビルドして実行します。

```
$ yarn build
$ yarn server
```

以下のように正しく動いていれば成功です！！

![5月-01-2018 17-42-41.gif](https://qiita-image-store.s3.amazonaws.com/0/80473/c5fd8ea9-e778-68a5-35df-98b44ffc04f5.gif)

## おわりに
jadeやejsなどを使わなければもうこれでSSR + SPAができてしまいます。
Next.jsなどを使うことでこのあたりも省略できてしまいますがフルスクラッチするなら試してもいいと思います。
少しでも参考になれば幸いです！