他のフレームワークと一緒に使う
HotwireやMPAのページの中にReactを埋め込むのは簡単です。Reactの公式サイトによると、Facebookも長らくこの使い方がメインでした。GitHubも同様です。GitHubの場合はTurbo中心で作られてページの中の一部分をReactで実装しています。
Apple StoreもMPAページの中にReactを埋め込んで使っています。ブラウザ側だけで製品のオプションを選択して、価格を表示しています。このような複雑なステートをフロンド側だけで管理するために使っているようです。なおAppleウェブサイトの他のページは、ほとんどがMPAになっています。必要なところだけReactを使っています。
一般的なページ、特にマーケティング的なページは、Reactの必要がありません。MPAでも十分ですし、ウェブデザイナーはMPAの方に馴染んでいることも多いでしょう。ほとんどのページをMPAで作り、複雑なステート管理が必要なところだけをReactで書くのは賢明な選択です。

本サイトでは何箇所かでHotwireのページの中にReactを埋め込んでいます。以下紹介します。
下記のUIを実装する例です。デモで実際に触っていただくこともできます。「variantを選択」のところで「react」を選択してください。
<% provide :head, javascript_include_tag("application_react_users", "data-turbo-track": "reload") %> <% set_breadcrumbs [["Users", users_path]] %> <% content_for :title, "Users" %> <div id="root"></div>
javascript_include_tag("application_react_users"...ではapplication_react_users.jsを読み込みます。これがReactアプリの本体です<div id="root">をセットしていますimport {createRoot} from "react-dom/client" import React, {useEffect, useState} from "react" document.addEventListener("turbo:load", () => { const root = createRoot(document.getElementById("root")) root.render(<UsersIndex />); }); function UsersIndex() { const [users, setUsers] = useState(null) const [selectedUser, setSelectedUser] = useState(null) useEffect(() => { fetch("/users", { headers: {Accept: "application/json"}, }).then(res => res.json()) .then(data => setUsers(data)) }, []) return ( <div className="grid grid-cols-2 gap-x-2"> {/* ... ページの内容はここ ... */} </div> ) }
turbo:loadイベントに応答して、先ほどの<div id="root">の箇所にUsersIndexコンポーネントを埋め込んでいます。turbo:loadは画面遷移が完了した時に呼び出される、Turboのカスタムイベントですturbo:loadではなくDOMContentLoadedイベントを使います。しかしHotwireはSPAですので、ページをリロードしないページ遷移をします。ページをリロードした時にだけ発火されるDOMContentLoadedよりも、TurboでSPA的にページ遷移しても発火するturbo:loadを使うのはこのためですApple Storeを模写した例です。デモはこちらに用意しています。
<!DOCTYPE html> <html> <head> <!-- ... --> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_include_tag "react_iphone", "data-turbo-track": "reload", type: "module" %> </head> <body> <div class="container container-lg mx-auto px-4 pt-16"> <div class="mx-auto min-w-[1028px] lg:max-w-5xl"> <div id="root"></div> </div> </div> </body> <% if @catalog_data %> <script type="application/json" id="catalog-data"> <% @catalog_data[:images].transform_values! { image_path(_1) } %> <%= @catalog_data.to_json.html_safe %> </script> <% end %> </html>
javascript_include_tag "react_iphone"でReactアプリの本体のreact_iphone.jsxを読み込んでいます<div id="root"></div>を設置しています<script type="application/json" id="catalog-data">の箇所ではカタログのデータ(オプションごとの価格など)をJSON形式に変換し、記載しています。これはカタログデータを読み込むためのブラウザからサーバへのリクエストを減らすためで、こうするとページロードの遅延を減らせます// ... document.addEventListener("turbo:load", () => { const dataJSON = document.getElementById('catalog-data').textContent const data = JSON.parse(dataJSON) const root = createRoot(document.getElementById("root")) root.render(<IPhoneShow catalogData={data}/>); }); function IPhoneShow({catalogData}) { // ... }
turbo:loadイベントが発火します。そして以下のことを実行します<script type="application/json" id="catalog-data">にあったJSONのデータを読み込み、dataオブジェクトにセットしますIPhoneShowコンポーネントにdataをprops(catalogData)として渡し、これを<div id="root">の箇所に埋め込みますDOMContentLoadedではなくturbo:loadにします。ただし事情によりturbo:loadが使えない場合は、そのページでそもそもTurboを読み込まないか、もしくはこのページに遷移するときはdata-turbo="false"属性を使うなどすると良いと思います<script type="application/json" ...>...</script>にデータを埋め込み、ERBからReactにデータを渡せます。Reactがサーバにデータをリクエストする回数が減らせますので、ページロードの高速化に繋がります
valuesを使ってエレガントに実現できます