2
6

More than 1 year has passed since last update.

Rails7とhtmでReactしてみる

Last updated at Posted at 2021-12-22

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
routes.rb
Rails.application.routes.draw do
  root 'home#index'
end

htmとreactの設定

ココからが本番ですね。
動画とは少し違うことをしますが、大差はありません。

importmapsの設定。

importmap.rb
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を読み込む。

application.js
import "@hotwired/turbo-rails";
import "controllers";
import "components";

マウント用のroot divを用意する。

app/views/home/index.html.erb
<div id="root"></div>

htmとreactを関連付ける。
rawという名前は、VSCodeのプラグインとの相性のためです。

app/javascript/components/raw.js
import { createElement } from "react";
import htm from "htm";

export const raw = htm.bind(createElement);

ハローワールド。

app/javascript/components/index.js
import { render } from "react-dom";
import { raw } from "./raw";

render(raw`<h1>hello world</h1>`, document.getElementById("root"));

この段階で画面表示はこうなってるはずです。

image.png

JSXが使えていますね。
……文字列ですが。
今のところ問題なし。

カウンターコンポーネントを追加する

とりあえず、とてもシンプルなカウンターコンポーネントを追加してみます。
動画ではclass componentを使ってましたが、hooksでもいけるみたいです。

app/javascript/components/Counter.js
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に渡しているのは文字列なので、自然とこうなるわけですね。

app/javascript/components/index.js
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との違いは、自作コンポーネントの呼び出し方法と中括弧の前に$をつけることくらいでしょうか。

image.png

useStateもちゃんと動作していますね。

VSCode拡張機能

lit-htmlというVSCodeの拡張機能を使えば、JSXは普通に書けました。
lit-html.tagsオプションが効かなかったのが玉に瑕ですが)

image.png

シンタックスハイライトも効いています。

Propsを試す

Propsを渡すために、特に意味のないコンポーネントを追加する。

app/javascript/components/RenderNum.js
import { raw } from "./raw";

export function RenderNum({ num }) {
  return raw`
    <div>${num}</div>
  `;
}
app/javascript/components/Counter.js
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を試してみます。

config/importmap.rb
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"
app/javascript/components/index.js
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")
);

image.png

あ……。

Invalid hook call. Hooks can only be called inside of the body of a function component.らしいです。

やり方が違うのか、出来ないのか判断できず、とりあえず諦めました。
詳しくて優しい人募集です。

感想

やや癖はありますが、意外といけますね。

(まだ使ったことはありませんが)stimulusが辛ければこちらを選択するのもありかも?

特にもしも外部Reactパッケージが使えるのであれば、かなり魅力的ではないだろうか。

検証に使ったコードはgithubにあります。

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6