LoginSignup
10
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-06

状況

  • 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 する

対象とするコード

/* @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 を取りうるとする。(redux-promise準拠)
  • store.dispatch を stub して action を収集する
  • 収集した非同期アクションを resolve して、 reducer に流す
  • 更新された store を使って再renderToString

実装した

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 をシングルトンにせずに引数で受け取って新しいキューを返すように引き回す。

使う


;(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 やってみたけど既にこういう実装ありそうなので調べる

10
3
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
10
3