renderToStringを２回実行してサーバーサイドレンダリングする

環境
---
* NodeJS v5.7.0
* Npm v3.7.1
* `babel-cli@6.5.1`
* [package.json](https://gist.github.com/59naga/0ea9d13b9d7597dc617e)

なぜ？
---
[renderToStringには、非同期処理を待つ機構が用意されていません](https://github.com/facebook/react/issues/1739)。_事前に非同期処理を済ませなければ_、空のstateでhtmlを生成します。

[^2]: 

たとえば、[redux-promise](https://github.com/acdlite/redux-promise/)を用いた以下のようなコードはうまく動作しません。
>記事中は`renderToString`ではなく`renderToStaticMarkup`を用います

```js:index.js
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import { Provider } from 'react-redux';
import Foo from './Foo';

const store = createStore(
  (state = {}, action) => {
    if (action.type === 'alreadyInitialized') {
      return Object.assign({}, state, { alreadyInitialized: true });
    }
    if (action.type === 'update') {
      return Object.assign({}, state, action.payload);
    }
    return state;
  },
  applyMiddleware(
    promiseMiddleware,
  ),
);

const provider = (
  <Provider store={store}>
    <Foo />
  </Provider>
);

console.log('state:', store.getState())
console.log(renderToStaticMarkup(provider));
```
```js:Foo.js
import React from 'react';
import { connect } from 'react-redux';

export default connect(
  state => state,
)(
  class extends React.Component {
    componentWillMount() {
      if (this.props.alreadyInitialized) {
        return;
      }
      // any asynchronous processing...
      console.log('mount props:', this.props);
      this.props.dispatch(new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            type: 'update',
            payload: {
              foo: 'complete',
            },
          });
        }, Math.random() * 500);
      }));
    }
    render() {
      return (
        <div>
          {this.props.foo || 'loading...'}
          {this.props.children}
        </div>
      );
    }
  }
);
```

```bash
babel-node index.js
# state: {}
# mount props: { dispatch: [Function] }
# <div>loading</div>
```

[koba04さんのReact.jsのComponent Lifecycle](http://qiita.com/koba04/items/66e9c5be8f2e31f28461)で解説されているように、nodejs上でhtmlを生成するさいも`componentWillMount`が呼び出されていることは確認できましたが、storeのデータ更新は`renderToStaticMarkup`より後に完了しています。

”_事前に非同期処理を済ませなければ_”、と書きましたが、方法が２つあります。

* コンポーネントの初期化時に非同期処理が必要なら、統一した`static`メソッド名（[async-props](https://github.com/ryanflorence/async-props)の`loadProps `など）に非同期処理を定義する。render実行前にこのメソッドを実行し、完了を待つ。
* `renderToString`を実行すると、renderした全てのコンポーネントは`componentWillMount`を実行するので、`componentWillMount`に非同期処理を定義し、`renderToString`で発行した非同期処理を監視し、完了を待つ。[^1]

[^1]: https://github.com/facebook/react/issues/1739#issuecomment-187328724

今回紹介するのは２つ目の方法です。

非同期処理を監視するreduxミドルウェアの作成
---

ということで、`renderToString`したときに`dispatch`された`Promise`の監視を行うミドルウェアを作成します。

```js:createPromiseWatchMiddleware.js
export default () => {
  const promises = [];// dispatch された全てのプロミス
  const middleware = () => (next) => (action) => {
    if (action && action.then) {
      promises.push(action);
    }

    return next(action);// actionをそのまま次のmiddlewareへ渡す
  };
  // 監視対象が全てfulfill時にfulfill
  middleware.wait = () => Promise.all(promises);

  return middleware;
};
```

> reduxミドルウェアの作成や振る舞いについては[yasuhiro-okada-aktskさんのRedux 基礎：Middleware 編](http://qiita.com/yasuhiro-okada-aktsk/items/1fe3df267a6034d970c1#middleware-%E3%81%A8%E3%81%AF)や[reduxの公式](http://redux.js.org/docs/advanced/Middleware.html)を参考にしました。

これをstoreに追加し、middlewareの提供する`wait`メソッドを使って、Promiseの完了を待ってから再描写します。

```js:index.js
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import createPromiseWatchMiddleware from './createPromiseWatchMiddleware';
import { Provider } from 'react-redux';
import Foo from './Foo';

const promiseWatchMiddleware = createPromiseWatchMiddleware();
const store = createStore(
  (state = {}, action) => {
    if (action.type === 'update') {
      return Object.assign({}, state, action.payload);
    }
    return state;
  },
  applyMiddleware(
    promiseWatchMiddleware,
    promiseMiddleware,
  ),
);

const provider = (
  <Provider store={store}>
    <Foo />
  </Provider>
);

renderToStaticMarkup(provider);

promiseWatchMiddleware.wait().then(() => {
  store.dispatch({ type: 'alreadyInitialized' });

  console.log('state:', store.getState());
  console.log(renderToStaticMarkup(provider));
});
```

```bash
babel-node index.js
# state: { foo: 'complete' }
# <div>complete</div>
```

２回目の`renderToStaticMarkup`で、`<div>complete</div>`と描写することが出来ました。

> `createPromiseWatchMiddleware`と同様の機能を提供するミドルウェアを[redux-hermit](https://github.com/59naga/redux-hermit#redux-hermit)という名前で公開しています。

express + react-router
---
ヘッドレスな（GUIの存在しない）クライアントから`/`にGETリクエストがあった場合に、上記の処理を実行するサーバーを実装してみます。

`store`は、リクエストの度に使い捨てることになるので、ファクトリ関数として定義し直します。

```js:configureStore.js
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import createPromiseWatchMiddleware from './createPromiseWatchMiddleware';

const reducer = (state = {}, action) => {
  if (action.type === 'alreadyInitialized') {
    return Object.assign({}, state, { alreadyInitialized: true });
  }
  if (action.type === 'update') {
    return Object.assign({}, state, action.payload);
  }
  return state;
};

export default () => {
  let promiseWatchMiddleware;
  if (typeof window === 'undefined') {// dosn't use at browser
    promiseWatchMiddleware = createPromiseWatchMiddleware();
  }

  const middlewares = [];
  if (promiseWatchMiddleware) {
    middlewares.push(promiseWatchMiddleware);
  }
  middlewares.push(promiseMiddleware);

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

  // expose promiseWatchMiddleware (for publish the .wait)
  if (promiseWatchMiddleware) {
    store.promiseWatchMiddleware = promiseWatchMiddleware;
  }

  return store;
};
```

[Server Rendering - react-router@v2.0.0](https://github.com/reactjs/react-router/blob/v2.0.0/docs/guides/ServerRendering.md)を参考に、`renderProps`が返った時だけhtmlを生成します。

```js:server.js
import express from 'express';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import { Provider } from 'react-redux';
import configureStore from './configureStore';
import Foo from './Foo';

const routes = [
  {
    path: '/',
    component: Foo,
  },
];

const port = process.env.PORT || 59798;
const app = express();
app.use((req, res, next) => {
  if (req.method !== 'GET') {
    return next();
  }

  // eslint-disable-next-line max-len
  const crawlerRegexp = /curl|wget|msie\s[6-9]|bot|crawler|baiduspider|80legs|ia_archiver|voyager|yahoo! slurp|mediapartners-google/i;
  if (req.get('User-Agent').match(crawlerRegexp) === null) {
    return next();
  }

  return match({ routes, location: req.originalUrl }, (error, redirectLocation, renderProps) => {
    if (error) {
      return res.status(500).send(error.message);
    }
    if (redirectLocation) {
      return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    }
    if (renderProps) {
      const store = configureStore();
      const provider = (
        <Provider store={store}>
          <RouterContext {...renderProps} />
        </Provider>
      );

      renderToStaticMarkup(provider);

      return store.promiseWatchMiddleware.wait().then(() => {
        store.dispatch({ type: 'alreadyInitialized' });

        return res.status(200).send(renderToStaticMarkup(provider));
      })
      .catch((reason) => {
        return res.status(500).send(reason.message);
      });
    }

    return next();
  });
});
app.listen(port, () => {
  console.log(`http://localhost:${port}`);
});
```

サーバーを起動した状態でcurlかwgetで結果を確認します。

```bash
babel-node server.js
# http://localhost:59798
curl http://localhost:59798
# <div>complete</div>
```

おわりに
---
`renderToString`を２回実行するので、`material-ui`のような重量のあるタグを生成するフレームワークで、数百件データを表示するようなページには向きませんが、`Promise`を使うことで簡潔に書けるため、可読性を高く保てる点がお勧めです。