redux で特定のアクションが来るまで SSR し続ける

## 状況

- A のロードが終わったら B をロードする
- B のロードが終わったら C をロードする
- C のロードが終わったら end を表示する

こういうものをSSRしたい。

## 問題

ReactDOMServer.renderToString() は同期しかとらないので、普通にやるだけだと A のロード待ちで止まってしまう。

この目的で使っていた https://github.com/recruit-tech/redux-async-loader が react-router v3 に依存していて、更新できていない。

## 解決策

- componentWillMount まで実行されるので、その action を収集する
- C のロード完了の action が実行されまで、store を更新しながら何度も render する

## 対象とするコード

```js
/* @flow */
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { connect, Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import promiseMiddleware from 'redux-promise'
import { compose, lifecycle } from 'recompose'

const initialState = {
  aReady: false,
  bReady: false,
  cReady: false
}

const reducer = (state = initialState, action) => {
  if (action.type === 'aReady') {
    return { ...state, aReady: true }
  } else if (action.type === 'bReady') {
    return { ...state, bReady: true }
  } else if (action.type === 'cReady') {
    return { ...state, cReady: true }
  }
  return state
}

const A = compose(
  connect(i => i),
  lifecycle({
    componentWillMount() {
      if (!this.props.aReady) {
        this.props.dispatch({ type: 'aReady' })
      }
    }
  })
)(props => {
  return (
    <div>
      A
      {props.aReady && props.children}
    </div>
  )
})
const B = compose(
  connect(i => i),
  lifecycle({
    componentWillMount(props) {
      if (!this.props.bReady) {
        this.props.dispatch(Promise.resolve({ type: 'bReady' }))
      }
    }
  })
)(props => {
  return (
    <div>
      B
      {props.bReady && props.children}
    </div>
  )
})
const C = compose(
  connect(i => i),
  lifecycle({
    componentWillMount(props) {
      if (!this.props.cReady) {
        this.props.dispatch({ type: 'cReady' })
      }
    }
  })
)(props => {
  return (
    <div>
      C
      {props.cReady && props.children}
    </div>
  )
})

const App = () => (
  <Provider store={store}>
    <A>
      <B>
        <C>end</C>
      </B>
    </A>
  </Provider>
)
```

やや冗長だが、自分自身に紐づく state が ready でなければ、それを読み出すという挙動。
b だけ Promise になっている。

## どう実装するか

- 事前条件として action は 非同期な Promise<Action> を取りうるとする。(redux-promise準拠)
- store.dispatch を stub して action を収集する
- 収集した非同期アクションを resolve して、 reducer に流す
- 更新された store を使って再renderToString


## 実装した

```js
let actionQueue = []
const hydrateQueue = async store => {
  let results = []
  for (const action of actionQueue) {
    console.log('>>>', action)
    const a = await store.dispatch(action)
    results.push(a)
  }
  actionQueue = []
  return results
}

const stubDispatch = store => ({
  ...store,
  dispatch(action) {
    console.log('<<<', action)
    actionQueue.push(action)
  }
})

const renderAsync = async (
  store,
  C,
  opts: { maxDepth: number, endAction?: string }
) => {
  let depth = opts.maxDepth
  let endFlag = false
  const endAction = opts.endAction

  let ret

  while (depth--) {
    ret = ReactDOMServer.renderToString(
      <Provider store={stubDispatch(store)}>
        <C />
      </Provider>
    )
    if (endFlag) {
      break
    }

    if (actionQueue.length === 0) {
      break
    } else {
      const execs = await hydrateQueue(store)
      if (endAction && execs.map(a => a.type).includes(endAction)) {
        endFlag = true
      }
    }
  }
  return ret
}
```

注意: ちょっと雑な実装なので、複数の非同期を扱う場合、スレッドセーフ(というかJSなので非同期セーフ)ではない。ちゃんとやるなら cationQueue をシングルトンにせずに引数で受け取って新しいキューを返すように引き回す。


## 使う

```js

;(async () => {
  const store = createStore(
    reducer,
    undefined,
    applyMiddleware(promiseMiddleware)
  )

  const ret = await renderAsync(
    store,
    () => (
      <A>
        <B>
          <C>end</C>
        </B>
      </A>
    ),
    {
      maxDepth: 5,
      endAction: 'cReady'
    }
  )
  console.log(ret)
})()
```

endAction がくるか、maxDepth回 render して返却する。

`<div data-reactroot="">A<div>B<div>C<!-- -->end</div></div></div>` が入手できるので勝利。

動くコード全文はここ https://gist.github.com/mizchi/ffe35ba2ad753a449b8159ed1121ce6b

## TODO

あとでライブラリにする or やってみたけど既にこういう実装ありそうなので調べる