サーバサイドで生成したReactコンポーネントをブラウザで再描画するときの処理を追ってみた


## TL;DR

チェックサムが毎回違うのでDOMを再レンダリングしない判定に失敗していたと思ったけどそんなことはなかったぜ。

## はじめに

ReactRubyというRubyからReactを呼び出すブリッジを作っている。

http://tanstaafl.0pt.jp/posts/2014/11/19/b22b24ca7ea5

https://github.com/minoritea/react_ruby

サーバサイドではRubyからExecJS経由でReactを呼び出して、描画している。サーバサイド、クライアントサイド両方で実行できるのがReactの一つの長所だと思っていて、Ruby製WebアプリでもReactを使いたいと思い作った。

さて、サーバサイドでHTMLを生成した場合、クライアントMVCで問題になるのがビューのレンダリングである。サーバ側でビューを実行しているのだから、クライアントではビューの再描画は実行せず、データに変更があった場合のみ更新処理が走ることが望ましい。

幸いReactでは、初回にロードしたとき、すでに存在しているHTML要素の中身が、コンポーネントのレンダリング結果と同じであれば、描画をスキップするという機能がある。

この機能では、ルートとなるコンポーネントごとにチェックサム値を持っていて、コンポーネントをレンダリングする先のDOM要素にもチェックサム値があれば照合して、合っていれば、再レンダリングなしでReactのコンポーネントをロードできる。

しかし、`React.renderToString`を実行すると、実行毎にチェックサム値が異なるので、自分のアプリではこの機能は使えていないのではないかと思っていた。

```ruby
ReactRuby.compile(jsx: "var Hello = React.createClass({render: function(){return <div>Hello, {this.props.user}</div>;}});")
ReactRuby.render("<Hello user='John' />")
#=> "<div data-reactid=\".k0wwacbaww\" data-react-checksum=\"196619668\"><span data-reactid=\".k0wwacbaww.0\">Hello, </span><span data-reactid=\".k0wwacbaww.1\">John</span></div>"
ReactRuby.render("<Hello user='John' />")
#=> "<div data-reactid=\".13eogc33eo0\" data-react-checksum=\"-720163874\"><span data-reactid=\".13eogc33eo0.0\">Hello, </span><span data-reactid=\".13eogc33eo0.1\">John</span></div>"
```

ところが、サーバサイドで生成した画面上でReactを実行するとどうもDOMの再生成は実行されていない、つまり、既存のHTMLを更新しているようである。

そこで、`render`メソッドを呼び出した時に実際にどのようなコードが実行されているのか追ってみた。

## react.js

### render

React.renderのコードを追って見る。

```javascript
render: function(nextElement, container, callback)
```

途中のバリデーションは飛ばす。

```javascript
var prevComponent = instancesByReactRootID[getReactRootID(container)];
```
`getReactRootID`はコンテナ要素の子要素の`data-reactid`属性値を取得している。
`instancesByReactRootID`は既存のコンポーネントのインスタンスをキャッシュする連想配列である。
キャッシュに既存のコンポーネント（`prevComponent`）が存在していた場合は更新処理がかかるようだがその部分は飛ばす（ReactのVirtualDOMに関しては他の方が解説してくれることを期待）。

関数の処理を下に追っていくとと、コンテナ要素にReactで生成済みの要素があるかを判別する部分がある。

```javascript
    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup =
      reactRootElement && ReactMount.isRenderedByReact(reactRootElement);

    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent;
```

コンテナ要素にReactで生成したElementが含まれているかどうかを判別している。
`getReactRootElementInContainer`は現在の実装だと単純に最初の子要素を返しているだけである（documentの場合はdocumentのルート要素）。

次に`isRenderByReact`の中身を見てみる。

### isRenderByReact

```javascript
  isRenderedByReact: function(node) {
    if (node.nodeType !== 1) {
      // Not a DOMElement, therefore not a React component
      return false;
    }
    var id = ReactMount.getID(node);
    return id ? id.charAt(0) === SEPARATOR : false;
  }
```
要素からIDを取得し、IDの1文字目がSEPARATOR（`"."`）かどうかを判別している。単純だ。
IDを取得する`ReactMount.getID`はコンポーネントのルート要素から`data-reactid`属性を取得している。

### render

元の`render`メソッドに戻ってさらに処理を追う。
上記から`shouldReuseMarkup`変数は、要素の子要素が`data-reactid`属性を持っており、かつ既存でReactComponentのインスタンスが生成されていないことを示す判定値が入っていることがわかる。


```javascript
    var component = ReactMount._renderNewRootComponent(
      nextElement,
      container,
      shouldReuseMarkup
    );
    callback && callback.call(component);
    return component;
  }
```

ここで`render`メソッドは終わる。`_renderNewRootComponent`が`render`メソッドの返り値であるReactComponentを生成する関数のようだ。
コンポーネントを生成する際、`ReactMount._renderNewRootComponent`を呼び出しており、そこに先ほどの`shouldReuseMarkup`の判定値が渡されている。
次に`ReactMount._renderNewRootComponent`の処理を追う。

### _renderNewRootComponent
```javascript
function(
        nextComponent,
        container,
        shouldReuseMarkup) {
```
途中のバリデーションは飛ばす。

```javascript
var componentInstance = instantiateReactComponent(nextComponent, null);
```
ここではReactElementからReactComponentのインスタンスを生成しているようだ。

```javascript
      var reactRootID = ReactMount._registerComponent(
        componentInstance,
        container
      );
      componentInstance.mountComponentIntoNode(
        reactRootID,
        container,
        shouldReuseMarkup
      );
```

`_registerComponent`は、`instancesByReactRootID`という連想配列に、ルートIDをキーに`conponentInstance`を登録した後、`reactRootID`を返している。
`_registerComponent`関数の中で、ルートIDを生成しているのは`registerContainer`関数で、この関数も、中で別の関数からルートIDを取得した後、連想配列に`container`を登録している。
結局どこでルートIDを生成しているのかというと、`ReactMount.getID`でコンテナ要素の最初の子要素の`data-reactid`を取ってきている。ルートIDが存在しない場合は、`ReactInstanceHandles.createReactRootID`を呼んでいるようだ。createReactRootIDはサーバーだと乱数、ブラウザだと連番でルートIDを生成している。

つまり、サーバサイドで生成したコンポーネントの`data-reactid`が毎回異なるのはランダム生成していたからである。
また、コンテナ要素の子要素の`data-reactid`属性がすでに存在していれば、それを使いまわしていることがわかる。

`mountComponentIntoNode`の中では`_mountComponentIntoNode`を呼んでいる。

### _mountComponentIntoNode

```javascript
    _mountComponentIntoNode: function(
        rootID,
        container,
        transaction,
        shouldReuseMarkup) {
      var markup = this.mountComponent(rootID, transaction, 0);
      mountImageIntoNode(markup, container, shouldReuseMarkup);
    },
```

`markup`は実際に挿入するHTML文字列だ。`mountComponent`で`markup`を生成するときに、rootIDを渡している。つまり既存の`data-reactid`属性があるのであれば、それを使いまわしている。

`mountImageIntoNode`内では、`shouldReuseMarkup`が真で、かつ`ReactMarkupChecksum.canReuseMarkup`でチェックサムを取り正しい場合は、何もしない。判定に失敗した場合は`markup`をコンテナ要素のinnerHTMLにセットする。

### canReuseMarkup
```
  canReuseMarkup: function(markup, element) {
    var existingChecksum = element.getAttribute(
      ReactMarkupChecksum.CHECKSUM_ATTR_NAME
    );
    existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
    var markupChecksum = adler32(markup);
    return markupChecksum === existingChecksum;
  }
```

まず、Reactのルート要素（コンテナ要素の最初の子要素）の`data-react-checksum`属性値を取る。
ここでようやく`react-checksum`が出てきた。
次に`markup`（HTML文字列）からadler32アルゴリズムでチェックサムを取り、比較する。

ここまででブラウザで`React.render`を呼んだ時に、サーバサイドで生成したコンポーネントが存在していた場合どういう挙動になるのかを追ってみた。

## 結論

`shouldRuseMarkup`が真になるのはコンテナの子要素が`data-reactid`属性を持っていた時だけなので、結論としてはサーバサイドで描画したコンポーネントは`data-reactid`属性を持ち、`react-checksum`の値がサーバ側で生成した値と同じであれば、コンポーネントの再描画は実行されないことがわかる。

`react-checksum`が毎回異なっていた理由であるが、サーバサイドで生成したHTML文字列も以下のようになるはずである（チェックサム属性は後で付与される）。

```html
<div data-reactid=".14sq0tlqadc" >...</div>
```

サーバサイドでdata-reactidを付与する際はランダムな値が付与される。このランダムな属性値を含むHTML文字列を元にチェックサムをとっていたため、毎回チェックサム値が異なっていたというわけである。

クライアントサイドで再描画の判定を行うときは、`data-reactid`はサーバーで生成された値を使いまわすため、チェックサムは成功しており、DOMの再描画はされていなかったということである。

## 感想

自分の勘違いで、時間を潰してしまったが、Reactの中身を追うことが出来たので有意義だった。
ただし、VirtualDOM Advent Calendarなのに一切Virtual DOMに触れていないので、次はVirtualDOMのコードも追ってみたい。