Hypernovaを使っていてクライアントでレンダリング出来ていなかったので解決した

github.com

HypernovaはSSRを実現してくれるライブラリ
個人で作ってるサービスで使っている
詳しくは一週間前くらいにRails DMで話したのでそちらを参照してください

blog.hatappi.me

hypernovaはhypernova/serverが落ちていてもコンテンツが表示されるようにclient側で実行されるようになっているのですが、これがうまく動いてなかったのが今回の問題

スペック

  • hypernova: v2.2.6
  • hypernova-ruby: v1.3.0
  • hypernova-react: v2.1.0

変更前の状態

今回は画面上部に配置するナビゲーションバーをReactでコンポーネント化しているのでそれを例にする

ディレクトリ構成としては次のような感じ
webpackではroot directoryとしてapp/javascript/componentsが指定されている

app/javascript/components
└── layout
     └── navbar.tsx

navbar.tsxコンポーネントはこんな感じ

import * as React from "react";
import { renderReact } from "hypernova-react";

class NavBar extends React.Component<{}, {}> {
   ~~~~
}

export default renderReact("NavBar", NavBar);

view側で呼び出す時は次のように呼び出している

render_react_component('layout/navbar.js')

hypernova server側はこのようになっている

const { environment } = require('@rails/webpacker')
const hypernova = require("hypernova/server");
const requireFromUrl = require("require-from-url/sync");

const config = environment.toWebpackConfig()

hypernova({
  devMode: process.env.NODE_ENV != 'production',
  port: 3030,
  getComponent(name) {
    if (config.devServer) {
      const t = new Date()
      const url = `http://${config.devServer.public}${config.devServer.publicPath}ssr/${name}?t=${t.getTime()}`
      return requireFromUrl(url).default
    } else {
      return require(`${config.output.path}/ssr/${name}`).default
    }
  }
});

この構成だとサーバーサイドではレンダリングされるけどクライアント側ではレンダリングされない

どう解決したのか

まず何故動いていなかったのかを出す前にどうしたら解決できたかを書く

コンポーネント側でexportする部分を次のように直すことでサーバーでもクライアント側でもレンダリングされるようになりました

-export default renderReact("NavBar", NavBar);
+export default renderReact("layout/navbar.js", NavBar);

なぜこれで解決するのか?

ここからはなぜ↑の解決方法で解決したのかをhypernovaの実装を見ていく

renderReact

まずコンポーネント側でexportする際に使っているrenderReactメソッドは次のように実装されている

https://github.com/airbnb/hypernova-react/blob/master/src/index.js#L6-L32

中身を見てもらうと分かるがserver、cliet側で呼び出された時のレンダリング用の処理が定義されている
server側ではReactDOMServer.renderToStringをclient側ではReact v16から追加されたReactDOM.hydrateがあればそれを使い、なければReactDOM.renderを使うようになっている

どちらを実行するかは次の部分で定義されているようにwindowオブジェクトがあるかないかで出し分けているようです

https://github.com/airbnb/hypernova/blob/master/src/index.js#L85-L89

render_react_component

view側で使用するrender_react_componentはhypernova-rubyが提供するhelperで役割としてはviewの中に複数のコンポーネントが存在する場合にそれらを識別できるようにマッピングして__hypernova_render_token[0]__のような文字列をviewに出力します

https://github.com/airbnb/hypernova-ruby/blob/master/lib/hypernova/controller_helpers.rb#L38

後はコントローラーでaround_filter :hypernova_render_supportを追加していると思うので、これが実行されることによってrender_react_componentによって追加されたコンポーネントをまとめてhypernova/serverにリクエストして帰ってきたHTMLを__hypernova_render_token[idx]__とリプレイスして返している

今回の問題だった点

今回は呼び出し側で render_react_component('layout/navbar.js') と定義していたので、生成されるdomは次のようになる

<div data-hypernova-key="layoutnavbarjs" data-hypernova-id="11111111-22222-33333-4444-534a36f354f9">~~~~~</div>

server側で実行される場合はrender_react_componentによってマッピングされたものを元にreplaceしていくので問題がないがクライアント側で実行される際には生成されたdomのdata-hypernova-keyを探してレンダリングする処理を行う

https://github.com/airbnb/hypernova/blob/master/src/index.js#L71-L83

そのため今回はrenderReact("NavBar", NavBar)と定義していたのでdata-hypernova-key='NavBar'を探してにいってしまってその要素が見つけられずクライアント側ではレンダリング出来ていなかった

最後に

今回はクライアント側で実行されなかった問題を調査して解決までいった