猫Rails

ねこー🐈

Hotwireの良かった点、辛かった点、向いているケース、向いていないケース

(自分はRailsを書くことが多く、フロントエンドの経験は乏しいです。見方にだいぶ偏りがあると思いますので、そのあたり差し引いてお読みいただければと思います〜🙇‍♂️)

こんにちは〜。Hotwireを仕事で使う機会があったので、実際に使ってみて感じた、良かった点、辛かった点、向いているケース、向いていないケースを共有します〜。

Hotwireとは?

まずはHotwireの簡単な説明を少しだけ(こちらの本のまとめです)。

HotwireはRails7からRailsのフロントエンドのデフォルトとなった技術です。TurboとStimulusという2つのJSのフレームワークから構成されます。Hotwireはこの2つライブラリの総称です。TurboはTurbo Drive、Turbo Frames、Turbo Streamsという3つの技術から構成されるため、Hotwireの登場人物は以下のようになります。

  • Turbo
    • Turbo Drive
    • Turbo Frames
    • Turbo Streams
  • Stimulus

(実際にはモバイルアプリ開発のためのTurbo NativeとStradaという技術もあるのですが、この記事ではWeb開発のための技術に絞ります。)

Turbo Driveとは?

通常の画面遷移

Turbo Driveによる画面遷移

Turbo Driveは画面遷移を高速にしてくれる技術です。

Turbo DriveはTurbolinksの名前を変えたもので、基本的な機能はTurbolinksと同じです。リンク、フォームのリクエストをTurbo Driveがインターセプトして、fetchによる非同期リクエストにしてくれます。そしてレスポンスされたHTMLの<body>要素だけを抜き出して、現在のページの<body>要素を差し替えてくれます。

通常の画面遷移がHTMLを丸ごと変えるに対して、Turbo Driveでの画面遷移は<body>だけを置換してくれます(正確には<body>の置換に加えて、<head>の一部がマージされます)。これの何が嬉しいかと言うと、画面遷移しても今のページのCSS・JSをそのまま利用できるため、CSS・JSを初期化してページに適用する処理をスキップできます。これによって画面遷移が高速になります。

Turbo Framesとは?

Turbo FramesはTurbo Driveの部分置換版です。

Turbo Driveが<body>要素全体を差し替えるのに対して、Turbo Framesは<turbo-frame>...</turbo-frame>というカスタム要素で囲った箇所だけを差し替えます。画面の一部だけしか更新しないような場合には、Turbo Driveの代わりにTurbo Framesを使うことで高速化できます。

上のGIFで詳細(_cat.html.erb)に相当する箇所を編集(edit.html.erb)に差し替えています。

Turbo Streamsとは?

Turbo Streamsは複数箇所のDOMを同時に更新できます。

Turbo Framesで更新できるのは<turbo-frame>で囲った1箇所だけという制約があります。そのため複数箇所を同時に更新したい場合にはTurbo Streamsを使います。

上のGIFでは編集(edit.html.erb)部分を詳細(_cat.html.erb)に差し替えて、さらにFlash_flash.html.erb)を新しいものに差し替えています。

あとTurbo FramesがDOMの置換しかできないのに対して、Turbo StreamsではDOMの追加・更新・削除をすることが可能です。

他にもActionCable(WebSocket)と組み合わせて使うことで、チャットのようなリアルタイムなアプリケーションを作れたりします。

Stimulusとは?

StimulusはHotwireでJSを書く際のレールのようなものです。生DOM操作の方法を提供してくれます。

Turboを使うとJSを書かずにサーバーサイドレンダリング + fetchでDOMを更新できるようになります。その結果、ReactやVueを使うのに比べて、JSを書く量は劇的に減ります。それでもJSが必要なケースは出てきます。そんな時には、Stimulusが用意するレールの上にJSを書くことになります。

MutationObserverでDOMの変更を監視して、振る舞いを与えるべきHTMLが検出されると自動でアタッチされます。そのためDOMを差し替えるTurboと相性が良いフレームワークとなっています。

Hotwireのデモ

デモ1は素のRailsで作った管理画面です(この記事では、HotwireやReact等のモダンなフロントエンドの技術を使わない、昔ながらのRailsのことを「素のRails」と呼ぶようにします)。

デモ1

デモ1はこちらから触れます

デモ2はHotwireを使いSPA風にした管理画面です。

デモ2

デモ2はこちらから触れます

デモ2ではデモ1の機能(ページネーション・ソート・検索・編集・登録・削除)をHotwireを使い画面遷移せずに行えるようにしています。デモ1のコードを(見た目の変更を除くと)数行修正しただけで、SPA風にできました。JavaScriptは1行も書いていません。

良かった点

Hotwireを使ってみて良かったと感じた点は以下のとおりです。思いつくままに書いたのであんまり整理されていませんが、ご容赦ください〜🙇‍♂️

サーバーサイドに集中できる

Hotwireの特徴はサーバーがHTMLをレスポンスするところにあります。

Hotwireを使うと、フォーム・リンクからのリクエストは全てfetchによる非同期リクエストになります。このfetchに対して、サーバーはHTMLをレスポンスします。

React(やVue等のJSライブラリ)を利用してSPAを作る場合には、fetchに対してサーバーはJSONをレスポンスして、クライアント側でDOMを構築することが多いと思います。このやり方だとバックエンドとフロントエンドで2つのアプリケーションを作ることになってしまいます。同じようなロジックをバックエンドとフロントエンドの両方に書く必要が出てきます。

Hotwireではfetchに対してJSONではなくHTMLをレスポンスします。これによってレンダリングはサーバーサイドでのみ行うようにできます。状態管理するのはサーバーサイドだけでいいし、モデルやバリデーションはサーバーサイドにだけ用意すればよくなります。プログラマはサーバーサイドに集中できるようになり、JSはサポート的に少しだけ使うもの(あるいはほとんど使わないもの)、という立ち位置になります。

チームもサーバーサイドエンジニアだけで構成できるようになります。

Railsの資産をフルに活かせる

レンダリングを全てサーバーサイドで行うというやり方は、昔ながらのRailsのやり方と同じです。自分の中にも会社の中にもRailsだったらこう書けばいい、というRailsの資産が沢山溜まっています。Hotwireを使った開発だと、その資産をフルに活かすことができます。

データモデリングとURL設計さえ適切にしておけば、コントローラーやビューはほぼ機械的に構築できる、というのがRailsの魅力の1つだと感じていました。テーブルとモデルが直接紐付かない場合でも、ApplicationModelを使うなどしてモデルさえしっかり作っておけば、コントローラーやビューはscaffoldと同じように作ればいいという安心感があります。なのでHotwireでそこを取り戻した感があって、ありがたい〜となりました。しかもkaminariやransackのようなモデルからビューを構築するためのお馴染みのgemも使えて、それらもTurboで簡単にSPA風にできるので、そこもありがたい〜となりました。

(HotwireはRailsに依存しない技術なので、Railsの部分はお好きなサーバーサイドのフレームワークに置き換えられると思います。ただ自分はRailsでしかHotwireを使ったことがないので、この記事ではRailsに限定して話をします。)

後付けで段階的にSPA風の挙動を追加できる

Hotwireの素晴らしい点の1つとして、既存のRailsアプリに対して後付けで段階的にSPA風の挙動を追加できる点があります。既存のRailsアプリのコードを少し修正するだけで、SPAのメリットを享受できるようになります。初めからHotwireを使って開発することも可能ですが、最初は慣れ親しんだ素のRailsで開発して、重要な機能だけをHotwireを使い作り込んでいくことが簡単にできます。デモでは、まずは素のRailsで開発してから(デモ1)、後付けでHotwireを使ってSPA風の挙動を追加しています(デモ2)。

少しの労力でSPAのメリットを享受できるからといって、全てのページをHotwireで作り込む必要はありません。全てのページが同じだけの重要度ではありません。例えば請求を見るためのページであれば、月に1度しか使われないかもしれません。そんな機能を作り込む必要はありません。Hotwireを使えば、重要でない機能は今までのRailsと同じように作り、重要な機能だけを作り込んでいく、ということが簡単にできます。

Turbo FramesやTurbo Streamsの利用は、少しだけとはいえ、アプリケーションに複雑さをもたらします。なのでまずはTurbo Driveを使い、さらに必要であればTurbo Frames・Turbo Streamsを使い、さらに必要であればStimulusを使うのがいいと思います。

まずは素のRailsで作っておいて、重要な部分だけを段階的に作り込んでいけることもHotwireの強みです。

参考

zenn.dev

fullstackradio.com

学習コストが低い

Hotwireは学習コストが低いです。TurboもStimulusも公式ドキュメントを読むだけなら数時間で読めます。周辺技術についても新しく学ぶことはあまりありません。Hotwire自体がRailsのエコシステムの一部であって、Railsで使っていたビュー構築のためのライブラリをそのまま使えます。

開発コストが低い

既存のRailsアプリのコードを少し修正するだけでSPA風にできます。デモ1からデモ2にするのに、(見た目の修正を除くと)数行しか修正していません。

WebSocketは必須ではない

自分は誤解していたのですが、WebSocketは必須ではありません。Turbo StreamsをWebSocket(ActionCable)でも使えるというだけです。WebSocketを使わずに、fetchリクエストに対してTurbo Streamsをレスポンスすることも可能です。多くのアプリケーションはこれで十分でしょう。WebSocketはチャットのようにリアルタイム性が必要な時にだけ使います。

WebSocketが必須となると考えることが増えて大変そうだな〜と思っていたので、WebSocketなしで使えるのはありがたいです。

辛かった点・辛くなりそうな点

Hotwireを使ってみて辛かった・辛くなりそうだと感じた点は以下のとおりです。

DOM更新時にレスポンスを待たないといけない

Hotwireはサーバーサイドでレンダリングを行います。そのためDOMを更新する際には、必ずサーバーからのレスポンスを待つことになります。Reactであればstateを更新するだけで済むような場合にも、レスポンスを待ってからDOMを更新します。DBのデータを更新する場合も、レスポンスを待ってからDOMを更新します。楽観的な更新はできません(一応Stimulusを使えば実現できますが、コスパ的にできるだけTurboに任せるのが良さそうです)。

高い応答性が必要な場合には、Hotwireはやめたほうが良さそうです。

SPAのユーザー体験とはだいぶ違う

HotwireはSPA風と呼ばれたりしますが、NextのようなSPAを期待すると裏切られます。ReactやNextでできることの多くがHotwireではできません。

Hotwireを使ってみると、これはSPAというよりも「素のRails」に近いもので、素のRailsに機能を+αするようなものだと感じました。具体的には素のRailsに対して、JSなしで画面遷移せずにCRUDできるようにして(Turbo)、JSから生DOMを触るためのレールを用意した(Stimulus)もの、というイメージです。実際には他にも色々なことができますが、コアな部分はそこなのかなと理解しました。画面設計もRailsと同じくCRUDをベースにしたものになり、SPAのような「アプリケーション」という感じのものとはちょっと違うと感じました(実際には作り込み次第です。ただRailsのやり方を大きく変えるものではありません。Railsのやり方から離れるほど、Hotwireの良さが消えていきます)。

自分が関わる案件だとこれで十分なことがほとんどで、まさにこういうものが欲しかったので、自分にはピッタリでした。ただ、ReactやNextで実現できるようなユーザー体験とは差があります。

SPAの2割の労力で8割の利益を取りに行くのがHotwireだと思います(実際に8割も得られるかはわかりません)。Turboを使ってSPA風にするのはとても簡単です。社内向けの管理画面のようなものであれば、Hotwireで十分です。でも一定以上のユーザー体験を実現しようと思ったら、Hotwireでは足りなくなる可能性があります。

Hey.comはHotwireで作られています。これ以上のものが必要かどうか?がひとつの目安になりそうです。

Herokuを使いづらい

HotwireはDOM更新時にレスポンスを待つ必要があるため、レイテンシの影響をモロに受けます。デモアプリのホスティングにはHerokuを利用しています。Herokuは東京リージョンがないためUSリージョンを利用しているのですが、日本からのアクセスだと200ms程度のレイテンシが発生します。通常の画面遷移だと(自分が関わる案件では)許容範囲になることも多いんですが、Hotwireを使って画面の一部を更新する場合にはちょっと許容できない遅さだと感じます。

こちらはデモ2のアプリをHerokuで動かしたものと、ローカルで動かしたものとの比較です。

Heroku(USリージョン)の場合
ローカルの場合

デモ2を触ってもらうとわかりますが、許容できない遅さです。

Herokuはよく使っていたので、これは残念です。Hotwireを使うなら東京リージョンがあるホスティングサービスを使いたいところです。PaaSだったらRender.comというサービスが安くて、使いやすくて、東京リージョンに対応予定という噂を聞いたので、試してみようと思います。他にも色々良さそうなPaaSがあるみたいです↓

blog.unasuke.com

あとHotwireとは直接関係ないのですが、Rails7からimportmap-railsがデフォルトになって、webpack等のバンドラーを使わずにES6のJSファイルをそのまま配信するようになりました。この際に多くの小さなファイルを送信することになるのでHTTP/2を利用するのですが、HerokuはHTTP/2をサポートしていません。これもちょっとHerokuを使いづらい理由になるのかな〜と思います。ただこちらに関しては、importmap-railsの代わりにjsbundling-railsを使ってバンドルするとか、Herokuの前段にCDNを置くとかすれば解決しそうです。

TypeScriptを使いづらい

HotwireでもTypeScriptを使えますが、ちょっと使いづらいです。

まずStimulus + TSだとtargetやvalue等のプロパティの型を自分で宣言しないといけないので、あんまりTSの恩恵を受けられていない感があります。

さらにRails7からimportmap-railsがデフォルトになって、トランスパイル・バンドルのプロセスがなくなり、ES6をそのまま使うようになりました。別にimportmap-railsを使わなければいいだけなのですが、せっかくならDHHおすすめのやり方でやりたい気持ちです。

あとHotwireを使うとロジックがサーバーサイドに寄ってJSは極端に少なくなるので、そもそもTSを使う必要性が低いということもあります。

Rails側もTSを使うことをあんまり考えていないような雰囲気を感じるので、現実的にはHotwireを使う際にはTSは使わないことが多いのかなと思います。

Stimulusが難しい

Turboは使いやすいのですが、Stimulusは難しいと感じています。正直、今のところStimulusを上手く使えている感がありません。

Stimulusを使う際のコツは、Stimulusコントローラーをページ単位ではなく機能単位で作ることだそうです。ページに対応する大きなコントローラーが1つあるのではなく、機能に対応する再利用可能な小さなコントローラーを沢山用意します。そして、それらのコントローラーを組み合わせてコンポーネント(という概念はありませんが)を作っていくイメージです。これを意識するようになってから、少しコードがマシになりました。

www.betterstimulus.com

Stimulusはドキュメントを読んで使うだけなら簡単なのですが、↑のサイトを読むなどして良い書き方を押さえておかないと、容易につらいコードを生み出します。

あとHotwireを使う際にはStimulus(というかJS)をできるだけ書かずに、サーバーサイドにロジックを寄せてTurboで処理するというのもポイントかなと思います。JSを書く量が増えるほどHotwireの良さが消えていき、React + TSを使いたくなります。

日本語の情報が少ない

日本ではまだあんまり使われていないようで、日本語の情報が少なく日本語での学習が難しいです。Railsのデフォルトに採用されてからまだそんなに時間が経っていないので、これから増えてくるのかな?と思います。

入門書を書きました。無料です。Hotwireの最初の1冊として読んでいただけますと幸いです🙇‍♂️

zenn.dev

黒魔術感

メタプログラミングの話ではありません。)

Turbo Streamsを使いDOMを更新する場合、コントローラーとビューはこんな感じになります。

# app/controllers/cats_controller.rb

  def update
    if @cat.update(cat_params)
      render # update.turbo_stream.erbをrender
    else
      render :edit, status: :unprocessable_entity
    end
  end
<%# app/views/cats/update.turbo_stream.erb %>

<%= turbo_stream.replace @cat %>

catの更新成功時にはupdate.turbo_stream.erbをrenderしています。update.turbo_stream.erbは以下のよう解釈されます。

<%# 内部的にはパーシャルのrenderが行われる %>
<%= turbo_stream.replace @cat do %>
  <%= render partial: "cats/cat",
             locals: { cat: @cat } %>
<% end %>

そしてこれは以下のようなHTMLになります。

<turbo-stream action="replace" target="cat_1">
  <template>
    <!-- パーシャルのrender結果 -->
  </template>
</turbo-stream>

最終的に<turbo-stream>カスタム要素がブラウザ側で評価されて、<template>内の要素で#cat_1の要素を置換します。

ちょっと独特なやり方で、初見時は何が起きているのかわかりませんでした。SJR(Server-generated JavaScript Responses)という、update.js.erbのようなファイルを用意して、JSをERBで作ってレスポンスするという方法がありましたが、それに似た不思議感があります。慣れるとRailsのレールに乗りつつパーシャルを簡単に使い回せていい感じなのですが、最初はちょっと戸惑うかもしれません。

Turbo Streamsに限らず、Hotwireは問題解決のアプローチが独特な感じがあるので、ちょっと黒魔術さを感じることがあります。

富豪的

HotwireのTurbo Framesは富豪的(というのかな?)なアプローチでSPA風の挙動を実現します。

デモ2ではページネーションにTurbo Framesを利用しています。ページネート時に緑線で囲った検索結果(一覧)の部分だけを更新します。

この際にindex.html.erbテンプレートを再利用するのですが、index.html.erbレンダリングするのは検索結果の部分だけではありません。検索フォーム部分も含めてHTMLを丸ごとレンダリングします。しかし検索フォーム部分はレンダリングされレスポンスされますが、クライアント側で使われずに捨てられます。実用上は問題にはなりませんが、ちょっと無駄な処理をしてるような感じもします。

ただ、この無駄のおかげで、既存のRailsアプリに1行コードを加えるだけで、画面を部分更新できるようになります。多少のパフォーマンスやサーバーリソースを犠牲にしてでも、問題をシンプルに解決して、生産性と保守性を取りに行くのがHotwire的なアプローチなんだと理解しました。

partialだらけになる

基本的にはDOM更新にはpartialを使うので、多用するとpartialだらけになります。うまいこと整理する方法を知りたい......

サーバーのリソースを食う

毎回サーバーサイドでレンダリングするので、サーバーのリソースを食います。

分業は難しい

バックエンドとフロントエンドが密結合するので、分業するのは難しいです。

あとモバイルアプリは基本WebViewになります。こちらも分業するのは難しいです。

これは1人のRailsエンジニアが上から下まで全部をできる、ということの裏返しです。どっちが良いかは作るもの・規模・リソース等によると思います。

後からSPAにするのは辛そう

規模が大きくなったらバックエンドとフロントエンドを分けてがっつりSPAにしたい、ということがあります。その時にHotwireを剥がしてSPAにするというのはちょっと辛いかもです。

Hotwireを使ったRailsアプリは素のRailsアプリとそんなに大きく変わりません。なのでコスト的には素のRailsアプリをSPA化するのとあまり変わらないと思います。ただ、結局フロントエンドとバックエンドを分けるんだったら、なんでHotwire使ったんだ?最初からReactにしておけばよかったのでは?となりそうな気がします。

向いているケース

Hotwireに向いているのは以下のようなケースになるのかな〜と思います。

素朴なUI

CRUDをベースにした素朴なUIを作るのに向いています。

JSをがっつり使ったリッチなUIを作るのには向いていません。

Hey.com以上のものが必要かどうか?がひとつの目安になりそうです。

サーバーサイドエンジニアだけの小規模なチーム

Hotwireを使うとサーバーサイドだけに集中できるようになります。サーバーサイドとフロントエンドの分業はなくなり、JSはサーバーサイドエンジニアが書くものになります。そのため、フロントエンドエンジニアがいないような、サーバーサイドエンジニアだけで構成される小規模なチームに向いています。

小規模なアプリケーションだと、一人の開発者がフロントエンドとバックエンドの両方を触れたほうが都合が良いです。逆に大規模な場合は分業したほうが都合が良いことも多く、Hotwireは向いていないのかなと思います(大規模なチームを経験したことがないので推測ですが)。

Railsに慣れているチーム

Hotwireを使うとRailsの資産をフルに活かせます。Railsに慣れているチームがHotwireを使うと、素のRailsアプリを開発するのに比べても遜色ない速度でSPA風のアプリケーションを開発できます(実際は作り込み次第です。デモ程度のものであれば素のRailsで作るのとほとんど労力は変わりません)。

管理画面

ここからは具体的なケースを考えてみます。

作るものとしては、Hotwireが一番向いていると思うのが管理画面です。管理画面はそんなに開発リソースをさけないけれども、そこそこインタラクティブであって欲しく、機能的にはCRUD + α程度におさまります。Hotwireを使うには丁度よいです。

Railsの受託会社

組織としては、Railsをメインにしている受託開発の会社はとても向いていると思います。

Railsエンジニアが多くて既存のRailsの資産を活かせるし、分業しない小規模なチームになることが多いし、素朴なUIでOKなことも多いし、価値を届けることにフォーカスできるし、受託案件の多くはHotwire向きです。実際に使うかどうかは案件次第となりますが、多くの案件で「丁度よい」選択になるんじゃないかと思います。

スタートアップ

自分はスタートアップの経験はあんまりないんですが、Autifyの近澤さんのpodcastを聞いて、Hotwireはスタートアップにも向いていそうだなと思いました。

anchor.fm

AutifyはRailsバックエンド + ReactフロントエンドによるSPAの開発を始めたのですが、開発速度の面で問題が生じたため、Reactを捨ててRailsのみにしたそうです。そしてフロントエンドエンジニアの方が入社されてから、効果が高いページから徐々にSPA化を始めたそうです(詳しくはPodcastを聞いてください)。

スタートアップではできるだけ速く価値を届けることが最優先事項になります。時間をかけてリッチなUIを作り込んでも、使う人がいなかったら意味がありません。最終的にSPAにするからという理由で最初からSPAを作るのは、場合によっては仮説検証のサイクルを回す速度を落としてしまう可能性があります(作るものやメンバーのスキルセット次第だと思います)。Hotwireは最初は素のRailsと同じように開発して、後付けで段階的にSPA風に作り込んでいくということが得意です。初速を落とさずにSPAの恩恵を受けられるので、開発速度と(そこそこの)ユーザー体験の両方を取りたいスタートアップには良い選択になります。

ただ、アプリの規模が大きくなったらバックエンドとフロントエンドを分けてSPA化したいというケースも出てくると思います。その際にHotwireを剥がすのは辛そうなので、結果的に中途半端な選択となってしまう可能性はあります。

個人開発

Hotwireは個人開発にも向いています。一人でバックエンドとフロントエンドの両方を書くのはなかなか大変ですが、Hotwireであれば一人でも問題なく開発できます。

向いていないケース

Hotwireに向いていないのは以下のようなケースになると思います。

  • リッチなUIが必要
  • 高い応答性が必要
  • フロントエンドとバックエンドを分業するような大規模なチーム
  • ネイティブアプリが必要(Hotwireだと基本WebViewになる)

Hotwireと受託会社

自分は受託の経験が長めで、HotwireはRailsの受託会社にとてもハマる技術だと考えているので、ちょっと受託とHotwireの関係を深堀りしてみます。

Hotwireをやらないなら、Railsをやる理由は減っていく

HotwireはReact(等の現代のフロントエンドの技術)とは真逆の方向を向いた技術です。RailsはHotwireの方向へ進んでいきます。つまりRailsは現代のフロントエンドとは逆方向へ進んでいきます。RailsをやるならHotwireをやりたいし、逆にHotwireをやらないならバックエンドにRailsを選ぶ理由は(既に減ってきていますが、今後ますます)減っていきます。なのでHotwireをやっていくかどうかという問題は、実は、今後も会社としてRuby/Railsをやっていくかどうかという問題と大きく関わっているんだと思います。

Reactをやっていきたい人からすると、Hotwireは受け入れられない感じになるんだろうと思います。そのためReactを既にガッツリ使ってる組織では、Hotwireは採用しづらそうです。Hotwireを中途半端に採用すると、Turbolinks + Stimulusがそうだったように、組織内で負債として扱われるようになってしまう可能性があります。なので、Hotwireを採用するなら、組織としてHotwireをやっていくぞ!今後もRuby/Railsをやっていくぞ!となるのが理想なのかなと思います。Railsメインの受託開発の会社だとその意思決定をしやすそうなので、その点でも向いていそうです。

Rails + Reactのつらさ

受託では分業せずに1人のエンジニアがバックエンドからフロントエンドまで全部やるケースが多いと思います。その時にRails + React(典型的にはNextによるSPA)を採用することが多いと思いますが、この構成にはつらさを感じています。

今まで自分が見てきたRails + Reactの案件は、あまりうまくいっていないことが多かったです。バックエンドとフロントエンドの両方を作る必要があり、開発速度の面で問題が生じていました。さらに開発速度の問題に加えて、複雑さが増すことでデバッグが難しくなり、多くのバグを生み出し、パフォーマンス改善が難しくなっていました。ユーザー体験を良くするためにReactを採用しているはずなのに、ユーザー体験を悪くしていることさえありました。

これはReactの問題ではなく、チーム構成と技術選定の問題です。自分が関わってきた案件は、Railsエンジニアだけから構成される小規模なチームです。そういうチームだとバックエンドはRailsありきで、フロントエンドどうしよう?という流れになります。バックエンドがRailsであれば、MPAよりSPAの方が複雑になりコストが高くなるのは明らかです。

でも、バックエンドもフロントエンドと同じくTypeScriptを採用すれば、そんなでもないのかな?と思ったりもします。言語を統一すればコンテキストスイッチがなくなるし、型やロジックを共有できます。つまりReactがつらかったんじゃなく、Rails + Reactがつらかったんじゃないかという気がしています。実際に仕事でTypeScript + React + Next + API Routes + Prismaのような構成で小規模なSPAを作ったのですが、フロントエンドもバックエンドもさくさく作れて、これで良さそうという気持ちになりました。面倒な部分はNextがいい感じにやってくれますし、なによりVSCode + TypeScriptの組み合わせによる開発体験がとても良くて、フロントエンドとバックエンドだけと言わず、全部TSでやりたい、くらいに思いました(API Routesで破綻しないような小規模なアプリを作った感想なので、規模が大きくなると話が変わりそうではあります)。

あと自分が見てきたRails + Reactの案件があまりうまくいっていなかった理由の1つとして、単純にReactやSPAに慣れていないということも大きかったです(というかこれが一番大きな理由な気がします)。バックエンドとフロントエンドの両方を学びつづけるのはなかなか大変です。基礎は押さえられたとしても、エコシステムはどんどん進化していきます。新しい情報を追いかけるだけで精一杯になってしまい、その結果技術の深堀りができずに、バックエンドもフロントエンドもどっちも中途半端ということになりがちです(なりました)。フロントエンドを実務レベルでやっていくには相当な学習が必要です。Nodeを深く知らずに、Nodeの経験を積まずに、フロントエンドをやることにはだいぶ厳しさを感じています。バックエンドにNode(TS)を採用すると言語やエコシステムの学習効率を最大化できるため、スーパーマンでなくともバックエンドとフロントエンドの両方を深く学習していけるようになるのかなと思います。

バックエンドをTSにするというのはとても良さそうです。TSはバックエンド、フロントエンドだけでなく、サーバーレス、モバイルアプリ、デスクトップアプリ(というかGUI全般)などあらゆる分野で活躍してくれるので、リソース不足の組織にも優しそうです。Hotwireのようにサーバーサイドレンダリングのみという制約による、ユーザー体験の限界もありません。型もあります。フロントエンドの流れで今後ますます進化していくことは確実ですし、実はこっちの方向性のが良いんじゃないかという気もしています。

分業できる規模ならRails + Reactでも問題にならないのかなと思います。ただ、一人で全てを担当するような場合には、Rails + Reactだと、RailsとReactのどちらの良さも殺してしまっているように感じます。Rails + HotwireでもReact + Node(TS)でもいいのですが、どちらかに寄せたい気持ちです。サーバーサイド重視ならRails + Hotwireで、クライアントサイド重視ならReact + Node(TS)を使うと作りやすそうですが、まぁどちらを選んでもバランスよく対応できると思います。

Railsの受託会社はHotwireが良さそう

とはいえ、Railsの受託会社にはHotwireを推したいです。

自分が関わった受託案件に関しては、Hotwireで十分なものがほとんどでした。パフォーマンスはたいていSQLボトルネックであり、数百msとか、場合によっては1000ms以上かかる場合があるとかそういうレベルです。そこを改善しておいて、バグが出ないようにして、そこそこのユーザー体験を実現できれば、あとは価値提供の速度が重視されます。

組織にはRailsアプリが沢山あり、Railsが得意なエンジニアが沢山いて、Railsの資産が溜まっています。Railsの受託会社の場合は、Ruby/RailsをやめてバックエンドをNode(TS)にしていく方向性よりは、Hotwireを採用する方向性のほうが良いんじゃないかという気がします。

Railsは技術的な目新しさはそれほどないかもしれませんが、サーバーサイドが重要なアプリケーションで、価値提供にフォーカスしたいのであれば、今でも良い選択だと思います。そしてRailsを使うならHotwireは最高の相方になります。

まとめ

えらい長くなってしまった。

Hotwireは向き不向きがはっきりした技術だと思います。Hotwireが良い選択になるかどうかはケース次第です。React等の現代のフロントエンド技術を使っていて、開発速度の面などで課題を感じていないのであれば、あえてHotwireを使う必要はないのかなと思います(たぶん1人のRailsエンジニアが上から下まで全部やろうと思った時に、この課題を感じやすいんだろうと思います)。

個人的にはHotwireは多くのRails受託案件で「丁度よい」選択になりそうだと思うので、受託会社の方にはぜひHotwireで遊んでみてほしいなぁと思いました。

参考

HTML Over The Wire | Hotwire

Stimulus 3 + Turbo 7 = Hotwire 1.0

Hotwireとは何なのか?

Hotwire を 本番環境で使ってみた感想 - Speaker Deck

Modern web apps without JavaScript bundling or transpiling

HotwireからDHHが考えるこれからのRailsとJSの付き合い方を知る - Speaker Deck

Bearer | Why Hotwire in 2021

Hotwireの感想 - laiso

Rails7がもつフロントエンドへの「答え」