Commentary
タブメニュー
タブメニューはTurbo Framesで作ることが多いです。Turbo Frames入門としては最適なUI要素です。
まずタブメニューの作る方に入る前に、Turbo Framesについて概略を説明します。
私の経験では、Turbo Drive, Turbo Frames, Turbo Streamsのうち、Turbo Driveはデフォルトで全てのページに適応されるので一番使います。ついで部分置換が必要な時は、Turbo Framesを使います。Turbo Framesでは難しい時に初めてTurbo Streamsを使います。ただしmorphingが導入されてからはTurbo Driveだけでできるものが非常に増えましたので、Turbo Streamsを使う頻度はさらに下がりそうです。
つまり、少なくとも私の通常の使い方であれば、Turbo Framesを習得すればTurboのほぼ全てが習得できます。
技術 | 用途 | 注記 | 使用頻度 (著者経験) |
---|---|---|---|
Turbo Drive | 全画面置換 | キャッシュ | > 90% |
Turbo Frames | 画面の部分的置換 | 関連機能が豊富 | ~ 15% |
Turbo Streams | 画面の部分的置換 | 複数箇所を同時置換可能 | < 5% |
Morphing | 差分的更新 | DriveやFramesと組み合わせて 変化したところだけ更新 | ~ 10% |
Turbo Driveがページ遷移、つまり画面全体を置換するのに対して、Turbo Framesはサーバから送られてきたデータを使って画面の部分置換をする時に使います。
「モーダル」「ポップアップ」「ドロップダウンメニュー」「ドロワーメニュー(引き出し)」「ライブ検索」、住所を入力するときに使う「階層メニュー」などは、どれもTurbo Framesで実装できます。
インタラクティブと言われるUI要素も、実は大部分はシンプルな部分的置換で実現できるためです。あとはブラウザの中でJavaScriptを使い、動きに変化をつけていくだけです。
Turbo Framesは部分置換だけではなく、これらのUI要素を作る時の周辺機能も提供してくれます。a
タグやform
タグとの連携、Lazy
loading (遅延ロード)、prefetch、URL同期、ローダー表示用のCSSなど、それに伴う便利機能もパッケージとして提供しています。
一方でReactなどの場合はuseState
フックや条件付きレンダーなどのパターンを提供してくれますが、これを組み合わせてUI要素を作るのは開発者しだいです。その意味でReactはフルスクラッチでUI要素を作成するのに適している一方、HotwireはUIライブラリとまではいかないものの、パッケージしたものを提供していると言えます。
MPA流のタブメニューはここでお試しいただけます。コードはGitHubでご確認いただけます。
早くTurbo Framesを使った実装を紹介したいところですが、その前にやっぱり 基本を振り返るおくことが重要だ と思います。基本とは、古典的なMPAを使った場合のタブメニューの実装方法です。
実は古典的なMPAでも、ほぼ十分なUIが実現できます。実際のデモを体験してください。
またTurbo Framesの開発の流れは、まず最初にMPAを作ることから始めることが大半です。Turboで開発する時はまずMPAから始めるべきだと覚えても90%間違いありません。
タブメニュー実装のタネ明かしは、タブより上の箇所が全く同じ2つの画面( users側、products側 )を用意しているだけです。タブの上の方は全く同じなので、置換されていることに気づきません。一方でタブの下の部分は異なる内容が表示されているので、ここだけが置換されたとユーザは錯覚します。
例えば食べログのサイトでも、このようなMPA流のタブメニューが今でも現役です。別の問題としてページの読み込みが非常に遅いのが気になりますが、UX的には十分に優れたものになります。
Turbo Driveによるタブメニューはここでお試しいただけます。コードはGitHubでご確認いただけます。
Turbo Driveを使う場合は、MPAのサイトにTurboのJavaScriptファイルをダウンロードするして、<head>
の中で読み込むだけです。
この場合はTurbo Driveによってヌルサクになった分だけ、タブの切り替えが自然に見えてきます。しかし実際にやっているのはMPAの場合と同様、 画面全体の置換です。
一見するとタブだけが差し代わっているようには見えますが、下記の点を細かくみると、実際には画面全体の置換だとバレてしまいます。
Turbo Framesによるタブメニューはここでお試しいただけます。コードはGitHubでご確認いただけます。
まず大切なことは、Turbo Driveを使った場合と比較したUIの違いです。一見するとTurbo Driveの場合とあまり差がありませんが、以下の点が異なります。
このようにTurbo Framesの特徴は画面を枠で分割し、枠内を置換しつつ、枠外をそのままに維持するところです。
Turbo Framesによるタブメニューの作り方はごく簡単です。まずはTurbo Driveのバージョンから出発します。そして、どこをTurbo Framesで囲むかを決めます。今回はSearchのテキスト入力フィールドの下のところからテーブルの最後までを囲むことにします。
次にエディタで該当するEJSファイルの内容を確認し、囲みたいところを <turbo-frame id="[適当な名前]"></turbo-frame>
のタグで囲みます。今回は2つのページ
(Users
とProducts
)がありますので、双方のEJSファイルで同じ処理をします。結果はtemplates/tabbed_segments_turboframes/index.ejsおよびtemplates/tabbed_segments_turboframes/products.ejsにあります。
Turbo Framesを実現しているのは下記の部分です。
tabbed_segments_turboframes/index.ejs
<turbo-frame id="tabs" class="turbo-with-loader" data-turbo-action="replace">
...
</turbo-frame>
tabbed_segments_turboframes/products.ejs
<turbo-frame id="tabs" class="turbo-with-loader" data-turbo-action="replace">
...
</turbo-frame>
はい、以上でおしまいです!
templates/tabbed_segments_turboframes/index.ejs
に<turbo-frame>
タグを加えるtemplates/tabbed_segments_turboframes/products.ejs
に同じIDの``タグを加えるdata-turbo-action
でURLと連動させたり、ロード時に自動的に追加されるbusy
属性を利用してCSSでローダーを表示したり、autoscroll
属性でスクロールの動作を変えるなど、機能を追加します。この3つのステップだけで、Turbo Frames的なタブメニューができ上がりました!
<turbo-frame></turbo-frame>
でくくることによって、Turbo
Frame中に含まれるa
タグやform
タグは通常と違う性質を持つようになります。通常であればTurbo Driveのような 全画面 遷移をするのですが、 全画面 ではなくて、同じTurbo
Frame内に限定された 部分画面 遷移をするように変化します。あたかもブラウザウィンドウの中に、もう1つ小さなブラウザウィンドウができたような感じです。
今回はTurbo Frameの中にタブがくるように配置しましたので、Users
, Products
のタブはTurbo Frame内を置換しながら切り替わっています。Turbo Frame内のみが変化するので、Searchのテキストフィールドもリセットされないわけです。
タブをクリックすると、通常のa
タグと同じようにHTTPリクエストは飛びます。そしてHTMLがサーバから返ってきます。ここからがTurbo Framesの動きです。通常なら画面全体を置換するのですが、Turbo Framesの場合は新しいページの中にある<turbo-frame></turbo-frame>
を探し出し、元のページの中にある<turbo-frame></turbo-frame>
の中身と置換するのです。この時、id
属性をみてturbo-frame
のペアを認識するので、id
の値を揃えておく必要があります。
今回、Turbo Framesは通常のa
タグで起動しています。a
タグなので、Turbo Driveの機能であるprefetchも働きます。このためprefetchによるUXの大幅向上、ヌルサクな体感も、何もしなくても勝手についてきます。
Turboの大きな特徴は、余計なHTMLをサーバから送ることを気にしないことです。タブを切り替えるだけならタブの中身だけをサーバから送信すれば良いのですが、今回のケースは常にページ全体を送っています。これは古典的なMPAによるタブメニューと完全に同じです。
実際問題、余計な箇所をサーバで際レンダリングしても大きな負荷にならないことがほとんどです。データベースに負荷のかかる処理は、通常はタブの中身になりますし、また余計なデータをネットワーク越しに送信しても、その負荷は一般に微々たるものです。そこの最適化をやるよりは、シンプルで行きましょうというのがTurboの考え方の一つになります。
コードの冗長さを嫌うのであれば、Rails ERB, Laravel Blade, Node EJSなどのHTMLテンプレートシステムに用意されている partial の機能などを使うことでDRYにできます。またサーバへの負担を心配するのであれば、サーバ側でキャッシュする機能などを使うこともできます。負荷があったとしてもサーバ側で十分に解決できますので、ページ全体を送ることは問題になりません。
Turbo Frameリクエストの場合は、HTTPヘッダーTurbo-Frame
が送られてくるので、これを見て余計なところを送信しないという選択肢もあります(turbo-rails gemを使えば、コントローラに#turbo_frame_request?
メソッドが用意されているので、これは容易にできます)。しかし上述のように、最適化の効果は微々たるもので、特に気にしないことが多いです。
また、タブのロードに時間がかかってしまう時のために、ローダーの表示もさせています。Turbo Framesはサーバに問い合わせするときにbusy
属性がつくのを利用します。
input.css
.turbo-with-loader[busy] .turbo-hide-on-loading {
visibility: hidden !important;
}
.turbo-with-loader {
position: relative;
}
.turbo-with-loader[busy] .turbo-hide-on-loading::before {
content: '';
visibility: visible !important;
display: block;
background-image: url('../../images/rocket.gif');
width: 64px;
height: 64px;
margin: 48px auto;
}
tabbed_segments_turboframes/index.ejs
<turbo-frame id="tabs" class="turbo-with-loader" data-turbo-action="replace">
...
<div class="my-10 px-4 sm:px-6 lg:px-8 turbo-hide-on-loading" >
<!-- [ここがロード中に隠れ、代わりにローダーが表示される] -->
</div>
...
</turbo-frame>
tabbed_segments_turboframes/products.ejs
<turbo-frame id="tabs" class="turbo-with-loader" data-turbo-action="replace">
...
<div class="my-10 px-4 sm:px-6 lg:px-8 turbo-hide-on-loading" >
<!-- [ここがロード中に隠れ、代わりにローダーが表示される] -->
</div>
...
</turbo-frame>
Reactによるタブメニューはここでお試しいただけます。コードはGitHubでご確認ください。
Reactのコードの特徴は以下の通りです。一般的なReactのデータフェッチのパターンを複数回使うだけで、ロジックは追いやすいと思います。
useState
を使用)Users.tsx
とProducts.tsx
)Users
、Products
コンポーネントの中のuseEffect
の中のfetch
で行うコンポーネントの切り替えはブラウザの中だけで行うので瞬間的ですが、データフェッチに時間がかかり、その間はローディング画面を表示します。そしてコンポーネントがDOMにロードされるまでuseEffect
は動きませんので、prefetchは効きません。
Next.js Pages RouterのSSRはページ全体を遷移するのには適していますが、Turbo Framesのように部分置換をする機能がありません。これは従来のSPAと同様に、ブラウザ上でuseEffect
を使ってステートを更新し、CSRで更新する必要があります。
一方でApp Routerは部分置換の仕組みが用意されています。LayoutとParallel Routesがこれに相当します。特にこのページで紹介しているタブメニューについては、Parallel Routesのドキュメントにわざわざタブメニューの言及もあり、この方法が奨励されていると受け取れます。
LayoutやParallel Routesを使うと、Client Componentを使わずにServer Componentだけでタブメニューが実装できます。App RouterではなるべくServer Componentを使うことが推奨されていますし、なるべくサーバでデータフェッチをすることがベストプラクティスとされています。LayoutやParallel Routesを使うと、これが可能になるわけです。
Parellel Routesを使ったデモも用意しましたのでご確認ください。またコードはGitHubにあります。
Parallel Routesを使った場合のUI/UXの特徴は以下の通りです
data-turbo-action
を設定した時と同じ効果です。data-turbo-action
を省略するだけで、URLを固定してタブを切り替えることが可能です。loading.js
を配置することでローディングアニメーションが出せます。配置しなければ、表示されません。loading.js
ファイルまでしかprefetchをしてくれません。そのため、コンテンツが表示されるまでにかかる時間は短くなりません。ReactでuseEffectを使った場合とほぼ同じUXになります。またParallel Routesを使った場合のコードの特徴は以下の通りです。
個人的には、随分と大袈裟なことをするなぁと感じます。タブは大小のものがありますし、厳密にURLと結びつく必要がありません。ルーティングと強く結びつくServer Componentとの相性の悪さを感じます。Client ComponentでuseState
、useEffect
を使った場合と比較して、UX上のメリットがありませんので、わざわざParallel Routesを使う動機は弱いと感じています。
今回はTurbo Framesによるタブメニューを実装しました。簡単なものだったため、UX的にMPAと大きな差はありませんでした。しかしTurbo Framesを使うと、枠の外のステートが維持できていることが確認できました。スクロール位置やフォーム要素のステート維持が必要な場合は、Turbo Framesを使う必要があります。
React、Next.jsと比較すると、Turbo Framesによるタブメニューは実装が簡単である一方で、機能的にも遜色がないことが確認できました。Turbo Framesの方がむしろ高速で多機能というケースが多いのもわかります。