Rails 7ではより容易に! Reactアプリを作成してみよう!
本記事では、RailsアプリにReactを組み込む中で、フロントエンド開発の基本的な流れをおさえていきます。
Reactは、UI構築に適したコンポーネント指向のJavaScriptフレームワークです。Rails 7が登場するまでは、ReactやVueといったJavaScriptフレームワークはバンドラーでアプリケーションに含めるのが普通でした。Rails 7では、importmap-railsによってReactモジュールを直接インポートすることが容易になっていますので、ここではこの方法によってReactを導入します。
作成するReactアプリはシンプルなものです。コントローラとアクションを1個作成し、そのビューのコンテンツをReactによって生成します。ただし、今回はReactによる開発手法の紹介が目的ではありませんので、コンテンツは最低限(メッセージと背景画像が表示されるのみ)にとどめています。
土台のアプリケーションを作成する
まずは、土台となるRailsアプリケーションを作成します。作成するアプリケーションのライブラリ構成は以下のとおりです(行頭の[○]はデフォルト構成のまま使うもの、[-]はデフォルト構成から外すもの、[+]は加えるもの)。
- [○]importmap-rails
- [-]Sprockets(sprockets、sprockets-rails)
- [+]Propshaft(propshaft)
- [-]Hotwire(turbo-rails、stimulus-rails)
アプリケーションは、rails newコマンドで作成します。今回は、アセットパイプラインにSprocketsに替えてRails 7から使用できるようになったPropshaftを使ってみることにします。また、Reactに集中するために同じくJavaScriptのライブラリであるHotwireのインストールをスキップすることにします。
アプリケーションの名前はreact_appとします。デフォルトのSprocketsに代わりPropshaftを使いますので、前節で紹介した-aオプションでpropshaftを指定しています。さらにHotwireのインストールをスキップしますので、--skip-hotwireオプションも指定しています。
アプリケーションが作成できたら、続く作業のためにcdコマンドでアプリケーションのフォルダに移動しておきます。
% rails new react_app -a propshaft --skip-hotwire …略… % cd react_app
この段階で、フロントエンド関連のファイルがいくつか生成されています。後ほどこれらに手を入れていきますが、どのようなフォルダやファイルがあるかここで見ておくことにしましょう(フロントエンド関連でないフォルダ、ファイルは省略しています)。
react_app ├── app │ ├── assets … アセットファイルを置くフォルダ │ │ ├── images … 画像ファイルを置くフォルダ │ │ └── stylesheets … CSSファイルを置くフォルダ │ │ └── application.css… アプリケーション共通のスタイルシート │ └── javascript … JavaScriptファイルを置くフォルダ │ └── application.js … アプリケーション共通のJavaScriptエントリポイント ├── config │ └── importmap.rb … importmap-railsの設定ファイル ├── public … アプリケーションの公開フォルダ │ └── assets … ビルドされたアセットが置かれるフォルダ └── vendor └── javascript … ダウンロードされたJavaScriptファイルが置かれるフォルダ
続けてrails generate controllerコマンドで、コントローラをアクションも指定して作成します。コントローラ名はreact、アクション名はhelloとします。
% rails generate controller react hello …略…
これで、コントローラapp/controllers/react_controller.js、ビューapp/views/react/hello.html.erbが作成されます。ルートページを、作成したreact#helloアクションに変更しておきましょう。
Rails.application.routes.draw do get 'react/hello' # ルートページをreact#helloアクションに root "react#hello" end
ここでrails serverコマンドでPumaサーバを起動し、「http://localhost:3000/」にWebブラウザでアクセスして、react#helloが呼び出されるのを確認しておきます。これで土台の準備ができました。
importmap-railsを設定する
Reactは、そのモジュールを直接インポートします。具体的には、importmap-railsに対してモジュールを「ピン留め」していきます。ピン留めとは、論理的なモジュール名をその実体(ローカルのJavaScriptファイルあるいはnpmパッケージなど)に関連付けることをいいます。ピン留めによって、インポートするモジュールを場所やバージョンに依存しない論理的な名前で指定できます。bin/importmapコマンドを使って、以下のようにピン留めします。
% bin/importmap pin react react-dom/client Pinning "react" to https://ga.jspm.io/npm:react@18.0.0/index.js Pinning "react-dom/client", to: https://ga.jspm.io/npm:react-dom@18.1.0/client.js Pinning "process" to https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/browser/process-production.js Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.21.0/index.js
今回使うReactのモジュールはreactとreact-dom/clientなので、これらを指定してbin/importmapコマンドを実行すると、自動的にパッケージの場所を割り出し、依存するパッケージも含めてモジュールをピン留めしてくれます。
ピン留めした結果は、importmap-railsの設定ファイルであるconfig/importmap.rbに反映されます。上記のbin/importmapコマンド実行後のconfig/importmap.rbは以下のようになっています。
pin "application", preload: true (1) # 以下が新たに追加された部分 pin "react", to: "https://ga.jspm.io/npm:react@18.0.0/index.js" (2) pin "react-dom/client", to: "https://ga.jspm.io/npm:react-dom@18.1.0/client.js" pin "process", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/browser/process-production.js" pin "scheduler", to: "https://ga.jspm.io/npm:scheduler@0.21.0/index.js"
これらは全て、ピン留めです。(1)は、デフォルトで用意されているモジュールのピン留め、(2)以降はReact関連モジュールのピン留めです。ここで登場するpin構文は、モジュール名に:toオプションで指定されるモジュール実体をピン留めします(同名の場合は:toオプションは(1)のように省略可能)。:preloadオプションは、モジュールと依存関係にあるモジュールを先に読み込むときtrueに設定します。
(2)では、モジュール"https://ga.jspm.io/npm:react@18.0.0/index.js"を"react"という名前にピン留めしてします。この設定により、モジュール名として"react"が指定されたときには、実体として"https://ga.jspm.io/npm:react@18.0.0/index.js"を使うというように対応付けられます。
config/importmap.rbに記述したピン留めは、ビューからはjavascript_importmap_tagsヘルパーメソッドの呼び出しでscript要素などに展開されます。JavaScriptからはimport文で参照することができます。具体的にどうなっているかは、本記事の後半で紹介します。
[NOTE]JSPM
bin/importmapコマンドは、既定でJSPMが管理するCDN(Content Delivery Network)からパッケージを検索し、モジュールのURLをピン留めします。JSPMは、Import Mapsのためのパッケージ管理とコンテンツ配布の機能を提供します。--fromオプションを指定することで、unpkgやjsdelivrといったCDNも選択できます。
% bin/importmap react --from jsdelivr # CDNとしてjsdelivrを指定 Pinning "react" to https://cdn.jsdelivr.net/npm/react@18.0.0/index.js
続けて、Reactのコンポーネントを作成していきますが、その前にコンポーネントファイルを入れるフォルダを作成し、そのフォルダもピン留めしておきます。この設定により、コンポーネントも置き場所を気にせずにモジュールとしてインポートすることが容易になります。app/javascriptにcomponentsフォルダを作成し、config/importmap.rbに以下の内容を追記します。
pin_all_from "app/javascript/components", under: "components"
pin_all_from構文は、指定したフォルダ以下のJavaScriptファイルを全てピン留めする指定です。:underオプションを指定すると、モジュール名にその値が前置されます。この場合は、"components/xxxx"というようになります。ここでは省略していますが、必要に応じて前述した:preloadオプションを指定できます。
Reactコンポーネントを作成する
Reactモジュールの直接インポートの準備ができましたので、Reactのコンポーネントを作成します。このコンポーネントは、ビュー中の指定するdiv要素に対して、「こんにちは○○さん!」というメッセージを設定するというシンプルなものです。コンポーネントのファイルは、app/javascript/componentsフォルダにreact_hello.jsとして作成します。先ほど設定したpin_all_from構文により、react_hello.jsは論理名"components/react_hello"でインポートできます。
import React from 'react' (1) import {createRoot} from 'react-dom/client' const Hello = props => ( (2) React.createElement('div', null, `こんにちは ${props.name} さん!`) ) Hello.defaultProps = { name: '名無し' } document.addEventListener('DOMContentLoaded', () => { const container = document.getElementById('app'); const element = React.createElement(Hello, {name: 'やまうちなお'}, null); createRoot(container).render(element); (3) })
(1)は、コンポーネントで必要なモジュールをインポートしています。このように、ピン留めした論理的な名前でモジュールを指定できます。(2)はHTML(この場合はdiv要素)を生成して返す関数Helloを定義しています。(3)はそれを呼び出してid属性が'app'である要素に戻り値を設定しています。
コンポーネントを読み込む指定は、app/javascipt/application.jsに記述します。このファイルは、アプリケーション共通のエントリポイントで、config/importmap.rbの先頭行にあるpin構文によってピン留めされています。そのため、ビュー中のjavascript_importmap_tagsヘルパーメソッドによってscript要素に展開されます。コンポーネントのモジュールは、pin_all_from構文で指定した「components」の付いた名前で指定できます。
import "components/react_hello" # 追加
そして、ビューにReactコンポーネントで更新されるdiv要素を追加しておきます。div要素のid属性の値であるappは、コンポーネント中で指定されていますから、両者は一致する必要があります。
<h1>React#hello</h1> <p>Find me in app/views/react/hello.html.erb</p> <div id="app"></div> # 追加
アセットを配置する
最後に、アセットである背景画像を配置します。画像ファイルの置き場所であるapp/assets/imagesにpig1.pngを配置し、app/assets/stylesheets/application.cssに以下のようにスタイルを追記します。このファイルは、アプリケーション共通のCSSファイルであり、ビュー中のstylesheet_link_tagヘルパーメソッドによってlink要素に展開されます。
body { background-image: url('pig1.png'); }
再びPumaサーバを起動し、図のようなページが表示されれば成功です。
[NOTE]JSXは使用できない
Reactでは、JSX構文でJavaScriptコードの中にHTMLを直接記述することができますが、これにはBabelによるトランスパイルが必要です。importmap-railsを使う場合にはJSX構文は使用できませんので、サンプルではHTMLをJavaScriptコードによって生成しています。JSXを使う例については、jsbundling-railsを使ったバンドラーの回で紹介する予定です。