Rails7襲来
Rails7がリリースされたということで、久々にTypeScriptから戻ってみたわけですが、どうやらフロントエンドに大きな変更が加わったらしく、色々調べてみました。
Rails7のフロントエンドのデフォルトはimportmapsという仕組みになるらしく、それは中々良さげな仕組みなのですが、説明する記事は多いので、この記事では触れません。
私の気を引いたのはこの動画でした。
なんと、Rails+importmapsでReactできるというのです。
htmというトランスパイル不要な、JSXを文字列で書くパッケージを使うようです。
ぱっと見かなり気持ち悪いのですが、食わず嫌いもなんなので、一度触ってみることにしました。
rails new
とりあえずごく普通のrails new
。
Rubyのバージョンは3.0.3。
適当なrootページを用意
./bin/rails generate controller home index
Rails.application.routes.draw do
root 'home#index'
end
htmとreactの設定
ココからが本番ですね。
動画とは少し違うことをしますが、大差はありません。
importmapsの設定。
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "htm", to: "https://cdn.esm.sh/v45/htm@3.1.0/es2021/htm.js"
pin "react", to: "https://cdn.esm.sh/v45/react@17.0.2/es2021/react.js"
pin "react-dom", to: "https://cdn.esm.sh/v45/react-dom@17.0.2/es2021/react-dom.js"
pin_all_from "app/javascript/components", under: "components"
下で用意するcomponents/index.jsを読み込む。
import "@hotwired/turbo-rails";
import "controllers";
import "components";
マウント用のroot divを用意する。
<div id="root"></div>
htmとreactを関連付ける。
rawという名前は、VSCodeのプラグインとの相性のためです。
import { createElement } from "react";
import htm from "htm";
export const raw = htm.bind(createElement);
ハローワールド。
import { render } from "react-dom";
import { raw } from "./raw";
render(raw`<h1>hello world</h1>`, document.getElementById("root"));
この段階で画面表示はこうなってるはずです。
JSXが使えていますね。
……文字列ですが。
今のところ問題なし。
カウンターコンポーネントを追加する
とりあえず、とてもシンプルなカウンターコンポーネントを追加してみます。
動画ではclass componentを使ってましたが、hooksでもいけるみたいです。
import { useState } from "react";
import { raw } from "./raw";
export function Counter() {
const [num, setNum] = useState(0);
return raw`
<div>
<button onClick=${() => setNum(num + 1)}>+</button>
${num}
</div>
`;
}
ちょっと変数展開の書き方が変わるくらいですね。
rawに渡しているのは文字列なので、自然とこうなるわけですね。
import { render } from "react-dom";
import { raw } from "./raw";
import { Counter } from "./Counter";
render(
raw`
<h1>hello world</h1>
<${Counter} />
`,
document.getElementById("root")
);
<${Counter} />
うう……。
なんとかいけました。
通常のJSXとの違いは、自作コンポーネントの呼び出し方法と中括弧の前に$
をつけることくらいでしょうか。
useStateもちゃんと動作していますね。
VSCode拡張機能
lit-htmlというVSCodeの拡張機能を使えば、JSXは普通に書けました。
(lit-html.tags
オプションが効かなかったのが玉に瑕ですが)
シンタックスハイライトも効いています。
Propsを試す
Propsを渡すために、特に意味のないコンポーネントを追加する。
import { raw } from "./raw";
export function RenderNum({ num }) {
return raw`
<div>${num}</div>
`;
}
import { useState } from "react";
import { raw } from "./raw";
import { RenderNum } from "./RenderNum";
export function Counter() {
const [num, setNum] = useState(0);
return raw`
<div>
<button onClick=${() => setNum(num + 1)}>+</button>
<${RenderNum} num=${num} />
</div>
`;
}
普通に渡せますね。
外部Reactパッケージを使ってみる
これが出来なければきついですね。
react-countupを試してみます。
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "htm", to: "https://cdn.esm.sh/v45/htm@3.1.0/es2021/htm.js"
pin "react", to: "https://cdn.esm.sh/v45/react@17.0.2/es2021/react.js"
pin "react-dom", to: "https://cdn.esm.sh/v45/react-dom@17.0.2/es2021/react-dom.js"
pin "react-countup", to: "https://cdn.esm.sh/v45/react-countup@6.1.0/es2021/react-countup.js"
pin_all_from "app/javascript/components", under: "components"
import { render } from "react-dom";
import { raw } from "./raw";
import { Counter } from "./Counter";
import CountUp from "react-countup";
render(
raw`
<h1>hello world</h1>
<${Counter} />
<${CountUp} end={100} />
`,
document.getElementById("root")
);
あ……。
Invalid hook call. Hooks can only be called inside of the body of a function component.
らしいです。
やり方が違うのか、出来ないのか判断できず、とりあえず諦めました。
詳しくて優しい人募集です。
感想
やや癖はありますが、意外といけますね。
(まだ使ったことはありませんが)stimulusが辛ければこちらを選択するのもありかも?
特にもしも外部Reactパッケージが使えるのであれば、かなり魅力的ではないだろうか。
検証に使ったコードはgithubにあります。