備忘録 blog

Docker/Machine Learning/Linux

RailsとReactを統合する方法を考える

なぜReactを導入したくなるか

現代のWebアプリケーションの発ではしばしば、リッチなユーザエクスペリエンスを提供するためなど様々な理由で、クライアント側に(ウェブブラウザ上に)状態を持っておきたいという状況があります。例えばチェックボックスにチェックが入っているかどうかで、他のDOM要素の内容が変わったり、モーダルが閉じているか開いているかを管理したり、などなど……。もちろん技術選択として、こうした操作が全く必要ない実装にすることも可能です。しかし、ボタンをトグルしたら画面上に何らかの変化があることをユーザが期待するというように、今やユーザビリティと、動的なウェブページは密接な関連があります。

さてこのとき、ユーザーの操作に応じて、HTMLの階層構造に含まれる1つ1つのタグで囲まれる要素を追加、削除、変更などする必要があります。こうした構造のことをDOM(Document Object Model)と呼びますが、クライアント側でDOMを操作するために、JavaScriptを書くことが事実上不可避となります。しかしながら素のJavaScriptを書くのはウェブブラウザ間の互換性の観点から、かなり大変です。例えば、このような場合に困りました。

event.target.matches('.heart')

これは、選択したDOM要素が、heartクラスかどうかを判定したいコードです。

developer.mozilla.org

これは、IEでは動作しません。なぜならばIEでは、matchesは非標準の名前 msMatchesSelectorというAPIで実装されているからです。またIEでなくとも、FirefoxChromeの古いバージョンのブラウザを利用しているユーザがいれば、このコードを正しく解釈できないかもしれません。これに対処するためには、古いバージョンのブラウザでも動作するように、互換性に配慮した関数定義に上書きするコードを導入する必要があります。これをpolyfillと呼びます。上記の例では、以下のコードがそれに対応します。

if (!Element.prototype.matches) {
    Element.prototype.matches = 
        Element.prototype.matchesSelector || 
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector || 
        Element.prototype.oMatchesSelector || 
        Element.prototype.webkitMatchesSelector ||
        function(s) {
            var matches = (this.document || this.ownerDocument).querySelectorAll(s),
                i = matches.length;
            while (--i >= 0 && matches.item(i) !== this) {}
            return i > -1;            
        };
}

しかし、JavaScriptで利用したい関数の全てに対してpolyfillを書くのは現実的には不可能です。こうした理由を含めて様々な理由から、クライアントサイドでJavaScriptを書く際にはjQueryと呼ばれる、ブラウザ間の差異を吸収し、様々な使いやすい関数を追加したライブラリが今まで広く用いられてきました。

ところが、たとえjQueryを利用したとしても、実装が困難な場合があります。例えば、

リモートサーバに置いてあるテーブルデータをajaxで取得する間は、テーブルをhiddenにしておくが、ロードが終了したらvisibleにする。けれども途中でロードを止めるボタンをクリックした場合は、その代わりに「ロードを止めた」という旨を表示する

という処理をしたいとします。このとき、ロードを止めたにもかかわらず、ajaxの結果が返ってきたコールバックが発火してしまうと、テーブルが表示され、かつ「ロードを止めた」旨も表示されるということになるかもしれません。このように、本質的に複雑な条件分岐を持つような場合には、とたんにバグ無く条件分岐を全てカバーするのが困難になります。言い換えると、操作AとBがあるとき、A→Bの順で行う操作と、B→Aの順で操作が行われるとき、その操作が冪等になってほしい時には冪等になり、条件付きの場合はその条件をどこか一箇所で管理したいという希望があるでしょう。

これが難しい理由は、jQueryで実装しようとすると、本質的に「現在描画されているDOMの情報を参照して、次のDOMを書き換える」という処理を行うことになるためです。しかし、描画に必要な情報が全てDOMとしてhtml上に現れてきているかというと、実際は必ずしもそうではないはずです。また、仮にDOM構造に現れていたとしても、それらの情報が散らばっていると、それを集めるのは依然として困難です。例えば検索ツールで、様々な検索フィルタを既に選択しているかどうかで取り消しボタンが表示されるか変わるといったことを考えると、それらの全てのフィルタに対して、DOM上で選択されているかどうかの条件分岐を書くよりも、内部に「選択したかどうか」という隠れ状態があって、その隠れ状態から今のDOMが構成されていると考えた上で、その隠れ状態で条件分岐を書く方が自然です。

そこで、クライアントの状態をstateとして持ち、そのstateに応じてレンダリングされるページが決まるとしたら、どうでしょうか。これはある意味ではMVCモデルで、Modelに状態を持ち、Viewがmodelの内容に応じて描画されるというのに似ていますが、Viewの内部にも仮想的に状態を持っており、その状態に応じてページをレンダリングするという機構を考えます。このとき、stateが変更されたときに自動的に必要となる部分の再レンダリングが行われるとします。そのとき、stateに対するビューの実装と、どのコールバックによってstateが変化するかという2点のみを私たちは考えればよくなります*1

このようなことが可能になるのがReactの利点の一つで、他のJavaScriptの類似のライブラリに比べて、JavaScriptとHTMLのDOMが密に連携した書き方ができるので、データフローの見通しが良くなり、モジュール化することも容易になります。そこで、Reactをクライアント描画のライブラリとして選択することにした、とします。

RailsとReactをどう統合するか

私の知る限り、ReactとRuby on Railsを統合するための方法は、おおざっぱに3つに分けられます。

1. Railsのsprocketsで管理する

いにしえのRailsでは*2JavaScriptcssや画像と共にsprokectsと呼ばれるアセットパイプラインで管理されていました。言い換えると、Railsの管理のもとでJavaScriptのコードを書くというスタイルです*3。この場合、JavaScriptのライブラリはnpmで導入するのではなく、著名なライブラリはrubyのgemとして頒布されており、それをRailsに組み込むという形で利用していました。

しかしReactを利用しようとなると、React上で実装されているawesomeなコンポーネントを利用したくなることが多くなると思います。こうしたライブラリはRubyの世界ではなく、JavaScriptの世界、つまりnpmのパッケージとして頒布されているものがほとんどですが、こうしたときに、npmで管理されているライブラリを自由に導入するのに困難があると考えられるので、javascriptのコードはモダンなビルド環境(webpackとnpm/yarn)の上で書きたくなります。

2. RailsAPIサーバとし、ReactはクライアントWebアプリとして個別に実装する。

サーバとクライアントを独立して実装するのは、特にプロトタイピングの場面では少し難しいことがあります。それは、アジャイル開発の過程で頻繁にAPIが書き換わるので、その変更に応じてクライアントとサーバを同時に書き換えないと不整合を起こすことがあるためです。特にこれはRESTのようなルーティングが分かれてカッチリしたAPIを設計しようとすると、この問題は強くなります。設計変更によるオーバーヘッドが大きくなってしまう一方で、開発初期から理想的な設計を常にできるとは限らないので、APIを切り替えるという手間が生じると、その分だけ開発速度が下がってしまいます*4

また特に、Webアプリケーションには、車輪の再発明が必要となる場面が多くあります。例えば、ユーザのセッション管理が必要だったり、CSRFトークンの受け渡しが必要だったり、ルーティングが必要だったりします。これらは、Railsのデフォルトの機能を使えば悩まなくてすむのに、自前で実装しようとするとバグを誘発してしまったり、実装がそもそも大変だったりしてしまうでしょう。ルーティングを含めたクライアントの実装を、全てJavaScriptの世界で完結させるならばそれでも良いですが、Railsのレールから外れることによる再実装のデメリットを甘んじて受け入れたくないというときは多いと考えられます*5

3. Rails上にReactを組み込み、rubyのパッケージ管理とjsのパッケージ管理を共存させる

そこで、Railsのassets pipelineと、webpackで管理されるReactのコードを共存するという方法が、三つ目に考えられます。この方法を利用すると、webpackでJavaScriptのライブラリを自由に導入できるという利点を残しながら、Rails Wayに則って開発を進めることができます*6

github.com

Reactにおける ReactDOM.render(element, container); が、ビューの内部で呼ぶことのできる react_component(ComponentName, Props, Options)というヘルパー関数に対応します。Railsのビューをエントリーポイントとしてpropsを渡すと、以後はReactの世界でコンポーネントを描画できるという形になっています。

まとめ

もちろんここで紹介した方法がベストとは限りません。動的なページが必要かどうかというところから疑うことも時には必要でしょう。RailsとReactの共存は、サービスの規模や開発者のリソースなどによって、取るべき選択肢は変わりますので、長所と短所を検討しながら、ベストな選択をしていくことが望ましいと考えられます。

参考文献

blog.toshimaru.net

*1:もちろんstateが複雑な入れ子になり、その更新が条件分岐を持つものになるように複雑になる場合は、Reduxのようによりデータフローを意識したライブラリを導入することが必要だと考えられます

*2:今もデフォルトではそうですが

*3:javascriptというより、CoffeeScriptとよばれる、alt-JSの一種を利用することが多かったです

*4:昨今ではGraphQLとよばれる、単一のエンドポイントでリソースの問い合わせが行えるようなクエリ言語が登場し、APIの設計をある程度柔軟にできるため問題を緩和してくれる可能性はありますが、まだライブラリ等は少なく、簡単に利用可能であるかというとわかりません

*5:例えば既にstaticに作ったサイトを改修するために、Reactで再実装するのは、限られたリソースの中では大変です

*6:もちろんその欠点として、RailsとReactが密結合になってしまうことによる弊害は様々あります