Rails×React×TypeScriptで作るWEBアプリ入門

この記事は、2021/8/19 に行われた WESEEK Tech Conference の内容をまとめたものです。

目次

はじめに

構成の概要

まず初めに今回紹介する構成の概要を説明します。

図を見ていただければわかる通り MVC フレームワークである Ruby on Rails の View 層に Wepacker を使って React やそのほかの JavaScropt をバンドルして erb などのテンプレートから呼び出して使うという構成になっています。

この構成を取ると基本的には Rails アプリケーションを作成することになるので、既存の Rails の知識や、すでに構築されている Rails アプリケーションに React を部分的に追加するなどという使い方が可能です。

記事中コードの利用バージョンについて

# ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
# rails -v
Rails 6.1.4
# node -v
v16.5.0

各種バージョンは発表時点の最新のバージョンを使用しています。Ruby が 3系であり特に新しいので他に使用している gem が Ruby3 に対応してないという可能性もあるので注意が必要です。

また、(前のバージョンに比べて)使ってる人がまだ少ないと思われるのでどこに落とし穴があるかわからないので最新版の利用でハマった場合は自己責任で。。。

他詳しいライブラリのバージョンなどはサンプルリポジトリの Dockerfile、Gemfile、package.json をご確認ください。
ちなみに自分がこの構成を本番環境で動かしているバージョンは以下になります。

# ruby -v
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-musl]
# rails -v
Rails 6.0.4
# node -v
v12.22.4

環境構築

Rails プロジェクトの用意

基本的に Webpacker の動くバージョン(Rails 5.2 以上) であれば新しくrails new しても既存の Rails プロジェクトを使っても問題ないと思われます。

自分の場合はローカルPCにあれこれインストールしたくないので基本的にはすべて Docker 上でやっており、以下のリポジトリを clone して VSCodedevcontainer から開くと利用バージョンで紹介したバージョンの rails new したばかりの Rails プロジェクトが用意できます。

ちなみにただの宣伝ではあるのですが devcontainer を使って Rails の開発をする解説みたいなものは自分の書いた以下記事が参考になるかと思います

React(react-rails) 導入

まずは Rails プロジェクトに React の導入をしていきます。

今回は react-rails という gem を利用して React を導入します。

やることとしては react-railsGet started with Webpacker に書いてあるコマンドをそのまま打っていきます。そうすると Webpacker がいい感じに設定を生成してくれてそれだけで React が使えるようになります。

以下は自分が実際に打ったコマンドです。

# edit Gemfile
gem ‘react-rails’ ←追加

# bundle install
# rails webpacker:install
# rails webpacker:install:react
# rails generate react:install
# rails g react:component HelloWorld greeting:string

このコマンドを打った時の差分が以下になります。

差分詳細

  • Gemfile, Gemfile.lock
    • react-rails を導入したための差分
  • app/javascript/components/HelloWorld.js
    • rails g react:component HelloWorld コマンドにて作成されたサンプルコンポーネント
  • app/javascript/packs/application.js, server_rendering.js, babel.config.js, config/webpacker.ym
    • webpacker 上で React を使うために必要な設定など
  • app/javascript/packs/hello_react.jsx
    • react-rails を使わない場合のコンポーネント例
  • package.json, yarn.lock
    • React を利用するのに必要な npm package が追加された差分

TypeScript 導入

続いて React(react-rails) の導入ができたので TypeScript も使えるようにしていきます。

こちらも react-rails が公式にサポート しているので先ほどと同様コマンドを打っていくだけで導入が完了します。

TypeScript の導入ができたのでこのタイミングで js で書かれたサンプルのコンポーネントを tsx に修正もしていきます。他にも rails new した時に作成された js で書かれたファイルを ts に書き直しても良いでしょう。

以下は自分が実際に打ったコマンドです。

# rails webpacker:install:typescript
# mv app/javascript/components/HelloWorld.js app/javascript/components/HelloWorld.tsx
# edit app/javascript/components/HelloWorld.tsx
 tsx かつFCコンポーネントに修正
# yarn remove babel-plugin-transform-react-remove-prop-types prop-types
# rm app/javascript/packs/hello_react.jsx

このコマンドを打った時の差分が以下になります。

コマンド上実施したことをわかりやすくするため edit コマンドなどを使っていますが、VSCode などのエディタ上での操作で問題ないです。

差分詳細

  • app/javascript/components/HelloWorld.js → HelloWorld.tsx
    • jsx のクラスコンポーネントから tsx のファンクションコンポーネントへ修正
  • babel.config.js, config/webpacker.yml, tsconfig.json
    • TypeScriptを利用するための設定
  • tsconfig には "allowSyntheticDefaultImports": true というオプションを追加し、import React from "react" という書式の inport 文を可能にするため追加します。
    • デフォルトだと import * as React from 'react'
  • package.json, yarn.lock
    • Typescript を利用するのに必要な npm package が追加された差分
    • 不要なライブラリの削除
      • prop-types 系は TypeScript にしたことにより、JavaScropt にはなかった型が言語的にサポートされるために不要なので削除してします
        • babel-plugin-transform-react-remove-prop-types
        • prop-types

React コンポーネント render テスト

すべての導入ができたので実際の描画テストをします。

rails g コマンドを用いて、サンプルの Controller と View を作成し、そこに react-rails gem にて生成された react_componet というヘルパーメソッドを利用して描画していきます。

# rails g controller Sample index
# edit app/views/sample/index.html.erb
<h1>Sample#index</h1>
<p>Find me in app/views/sample/index.html.erb</p>
<%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %> ←追加
# rails s -b 0.0.0.0
…
Listening on http://0.0.0.0:3000

  • props に Hello from react-rails. を渡したサンプルのコンポーネントが描画されています。

react_component について

react-rails gem を導入するとにょきにょき生えてくる react_component ヘルパーメソッドについて軽く説明します。

とはいえあまり詳しいことを説明してもそこまで役には立たないと思うので、簡単に言うと、指定したディレクトリ配下に置いたコンポーネントの名前で紐づけていい感じに ReactDOM.render してくれるもの程度で実務上は問題ないと思います。

指定したディレクトリというのはデフォルトだと app/javascript/components 配下で自動的に application.js に設定が追加されています。

react_component(react-rails) を使うメリット

利用のメリットを挙げるとすると以下の3点のようなものがあげられると思います。

  • erb(テンプレート)の render したい場所に直接直感的に書ける
  • erb に書けるため、Rails から props を渡すのが容易
  • react-rails を使わない時に比べて記述量が少ない

メリットだらけだと個人的には思いますがあえてデメリットを上げるとすれば、erb と React コンポーネントの定義が原則別ファイルかつ、明示的な import とかを必要としないので紐づける手掛かりが両方べた書きの文字列になってしまうくらいかと思います。

react_component を使わない場合

逆に react_component を使わない場合ですが、デメリットばかり書くことになってしまいますが以下のようなものが挙げられます。

  • ReactDOM.render を js ファイル側に書かなければいけない
  • erb などのテンプレートに直接書かないため render する場所の指定が直観的ではない
  • erb などのテンプレートに直接書かないためRails 側から props を動的に渡すのが若干困難
  • react_component を使う場合に比べて記述量が多い

メリットとしてはコンポーネントの定義と render が同じ JavaScropt ファイルに書くことになるので先ほど使う場合のデメリットに挙げた、紐づけがべた書きになることはなく、一応補完が効くというメリットもなくはないかなと思います。

個人的には素では使わず react-rails みたいな gem を使って React の描画をするのが良いと思っています。

Tips

上記紹介した構成を実際に運用してみて行った改善などを Tips として紹介します。

Rails から React コンポーネントへの props の渡し方

react-rails のサンプルにある例

react-rails の react_component を公式のサンプル通りに使おうとすると以下のようになります。

  • erb
    <%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %>
  • コンポーネント
    type Props = { greeting: string }
    const HelloWorld: React.VFC<Props> = ({ greeting }) => <>Greetings: {greeting}</>

greetung という string の props を1つだけ受け取ってそれを表示するコンポーネントです。

props が大量にある場合の例

props が大量に増えたらどうなるでしょうか。

type Prefecture = '東京都' | '京都府' | '大阪府' | '北海道'

type Props = {
  username: string
  email: string
  age: number
  prefecture: Prefecture
  address: string
  prefectureList: Prefecture[]
}

const ProfileForm: React.VFC<Props> = (props) => {

上記コンポーネントはプロフィールを編集するためのフォームを想像していただければと思います。

props には編集するためのユーザの情報と、 prefecture はリストから選ばせたいのでマスタとなる prefectureList を受け取るような設計になっています。

実際はこんな感じにたくさんの props をバラバラと受け取らず、モデルやまとめた Object とかを渡すかと思いますがあくまでも例です。

コンポーネント利用するためのデータ、コントローラ用意

実際にサンプルで用意した props を大量に受け取るコンポーネントを利用するための準備をします。

こちらは純粋な Rails のみの機能なので説明は割愛させていただきます

  • サンプル用に Prefecture, User モデルを定義
    rails g model Prefecture name:string
    rails g model User name:string email:string age:integer address:string prefecture:references
    rails db:migrate
  • データ投入
    rails c
    > Prefecture.create(name: '東京都')
    > Prefecture.create(name: '京都府')
    > Prefecture.create(name: '大阪府')
    > Prefecture.create(name: '北海道')
    > User.create(name: 'haruhiko', email: 'haruhiko@weseek.co.jp', age: 30, address: '高田馬場', prefecture: Prefecture.first)
  • controller 編集

    class SampleController < ApplicationController
    def index
    @prefecture_list = Prefecture.pluck(:name)
    # ["東京都", "京都府", "大阪府", "北海道"]
    
    @user = User.find_by(name: 'haruhiko')
    # id: 1,
    # name: "haruhiko",
    # email: "haruhiko@weseek.co.jp",
    # age: 30,
    # address: "高田馬場",
    # prefecture_id: 1,
    # created_at: Fri, 13 Aug 2021 00:40:29.546785000 UTC +00:00,
    # updated_at: Fri, 13 Aug 2021 00:40:29.546785000 UTC +00:00
    end
    end

View

<%= react_component("ProfileForm", {
  username: @user.username,
  email: @user.email,
  age: @user.age,
  prefecture: @user.prefecture.name,
  address: @user.address,
  prefectureList: @prefecture_list 
}) %>

このくらいであればまだ許容範囲かもしれませんが、このようにすべての props に対して一つ一つ値を渡さなければいけなくなり、中身が複雑になればなるほどテンプレートがごちゃごちゃしてきます。

Jbuilder gem を利用する

テンプレートがごちゃごちゃする問題を解消するには Rails を使ってる人にはおなじみの Jbuilder gem を使うと、テンプレートに props の定義を書く必要がなくなり記述がスッキリするだけではなく、props の成形は専用のライブラリへと責務の移譲ができます。

先ほどの View を書き直すとこんな感じになります。

# /app/views/sample/_profile_component.json.jbuilder

json.username @user.username
json.email @user.email
json.age @user.age
json.address @user.address
json.prefecture @user.prefecture
json.prefectureList @prefecture_list
<%= react_component("ProfileForm", render(template: 'sample/_profile_component.json')) %>

User の情報を一つの Object にまとめる場合と注意点

実際にコードを書く際には、以下のように Rails 側で生成した User モデルをひと固まりとして、React コンポーネント側で ViewModel みたいなものを定義する場合が多いかと思います。

type PrefectureName = '東京都' | '京都府' | '大阪府' | '北海道'
type User = { username: string; email: string; age: number; prefecture: PrefectureName; address: string }

type Props = { user: User; prefectureList: PrefectureName[] }
const ProfileForm: React.VFC<Props> = ({ user, prefecrureList }) => {

この場合の Jbuilder

json.user do
  json.extract! @user, :username, :email, :age, :address
  json.prefecture @user.prefecture.name
end
json.prefectureList @prefecture_list

jbuilder の書き方の話ですが extract! メソッドを使い、必要なカラムのみを user インスタンスから抜き出しています。

実はこの場合 erb 側に書く react_component メソッドには以下のように user インスタンスを直接渡しても受け取る側の構造が一致すれば描画することが可能です。

<%= react_component("ProfileForm", {
  user: @user,
  prefectureList: @prefecture_list 
}) %>

しかしこの場合 @user の中身には idcreated_at などの描画に必要のない値まで入ってしまっていて、これらの値は javascript の中に含まれてしまうため、ブラウザのデベロッパーツールなどを使えば除くことが可能です。

したがって今回の例は別に漏れてもそこまで問題にはならないのですが、もし渡すインスタンスのカラムの中に IP アドレスやパスワード、token などの秘匿情報が含まれていた場合情報漏洩につながるので気を付けなければいけません。

なので、Jbuilder のような JSON 成形のライブラリを必ず通すようにして必要なカラムのみを抜き出して props に渡してあげるようにしています。

おすすめのディレクトリ構成

基本的には Rails 標準及び、webpacker:install して出来た通りに使っていて、中でも必要だと思い独自に作成したディレクトリの紹介をします。

とはいえ今回紹介するのは /app/javascript/components 配下のみです。

他にも services といったディレクトリを切ってサービス層の追加などをやってはいるのですが、こちらは Rails の中で完結し、今回は関係ないので割愛します。

components 配下

デフォルトだと /app/javascript/components というディレクトリがただ一つ作成されるのみで、最初はその中に何となくのディレクトリを切って適当にコンポーネントを入れていました。

そうするとそこそこ無秩序にディレクトリやコンポーネントの数々が増えていき管理が大変になってきました。

そこでcomponents の配下に新しく以下ディレクトリを追加しました。

  • pages
  • commons

pages 配下

pages の配下は以下のルールでコンポーネントを置いています。

  • pages 配下に置いたものは原則 erb からのみ呼び出す
  • いわゆるコンポーネントとしての部品ではなく page 全体を構成するもの
    • 通常 MPA である Rails ではあるが、React を使った SPA みたいな動きをするページの root になるようなコンポーネント
    • next.js における pages 配下に配置されるようなコンポーネント
  • pages 配下に置いたコンポーネントと同名のディレクトリを切ることもある
    • 配下にはそのディレクトリ名のコンポーネントからしか呼び出されないことが確定しているようなコンポーネントを置く
    • UserForm というコンポーネントがあれば UserForm ディレクトリ配下には UserForm コンポーネントから飲み呼び出されるようなコンポーネントが置かれる
      • pages/UserForm.tsx
      • pages/UserForm/UserNameFormInput.tsx

commons 配下

  • あらゆるerb、コンポーネントから一部品として呼ばれるコンポーネント
    • commons/badge/UseBadge.tsx

この画像のコンポーネントは実際に業務で使ってるコンポーネントであらゆるところから呼ばれて利用されているバッジコンポーネントです。

他にもデザインを共通化したいようなフォームの部品であったりボタンとか割と大部分が入ってくると思います。そのためにバッジみたいなサブディレクトリが切られることが多いでしょう。

アーキテクチャについて

最後に今回紹介した Rails の上に React を乗せる場合と、詳しくは説明しませんでしたが比較のために Rails と React を別々に分離する場合のアーキテクチャの説明と比較をします。

アーキテクチャ選定の助けになればと思います。

Rails の上に React を乗せる場合

全体的な構成の概要は一番最初の 構成の概要 を参照ください。

メリット

最初の概要の説明でもした通り、Rails アプリケーションを作成することになるのでそれにちなんだメリットが多いと思われます。

  • Rails の機能のほとんどをベースに React を使うことになる
    • 既存の Rails アプリケーションにちょい足しすることもできる
    • 既存の Rails の知識がほぼすべて活かすことができる
  • Rails の認証基盤がそのまま利用できる
    • 分離した場合にはクライアントアプリとのつなぎ込みを考慮しなければない。

中でも既存の Rails が使えるということは昔から存在する Rails プロジェクトを拡張することが可能ということなので、今までの資源を余すことなく利用することができます。

デメリット

  • Rails の上に React という全く別の物が乗る
    • Ruby ではなく JavaScript のライブラリ
    • Rails の弱いところである View 層
      • それを補うはずだった Webpacker 特有の諸々
  • Next.js といった フロントエンドフレームワークが原則使えない
    • SPA が作りづらい

デメリットとして個人的に一番大きいと思っているのが2番目に挙げたフロントエンドフレームワークが利用できないところだと思っています。

やはり餅は餅屋であって、フロントエンドを専門に考えられて作られるフレームワークのほうが技術的に発展が早かったり高度であることが多いのでその恩恵を受けづらいのはもったいないなと思います。

また Webpacker ですがこちらもカスタマイズをしようとすると癖が強く、皆苦労しているという印象を受けます。とはいえ React を使えるように導入するだけであればデフォルトの設定でも問題はなく、この問題に直面するのは何か凝ったことをやりたくなる時かと思います。

Rails と React を別々に分離する場合

構成の概要

先ほどは全部 Rails の枠の中にすべてを乗っけていたのですが、クライアントアプリ、つまりブラウザで動かすものと、Rails(APIサーバ)として分離しています。

クライアントアプリの JavaScript から Rails の API を呼び出し Rails の API はクライアントからリクエストを受け取ったら Jbuilder などで JSON を成形して JSON を返すという流れになります。

メリット

メリットとしては何よりも分離をしているので互いに得意なことへ徹することができるという点にあると思います。

  • Rails では API の提供に集中
  • フロントはモダンなフロントエンドフレームワークの利用ができる
  • 評判の良くない Webpacker を触る必要が無い
    • 生の Webpack もそれはそれでツラいが。。。
  • バックエンド/フロントエンドの分業が容易

挙げた通り餅は餅屋であり、それはフレームワークはもちろんのことエンジニアの分業も比較的可能になるのでそれぞれのスペシャリストが担当し、開発スピードを上げるといった戦略を取ることも可能になるでしょう。

デメリット

メリットの項では分離することがメリットと紹介しましたが、分離されてるからこそのデメリットがあげられます。

  • リポジトリが増えたり、ディレクトリが増えたりする
  • Rails と フロントエンド二つの技術を使うので学習コストが高い(かも)しれない
    • かもというのは Rails に乗せた場合も結合するための Webpacker など独自のルールに対する学習コストがかかってくるのに一概にもどちらが高いとは言えない
  • 分業が出来てもコミュニケーションコストが増えることもある
    • 分離されたアーキテクチャの結合部分の調整
  • 認証が必要な場合工夫が必要になる
    • API サーバとは http でやり取りをするため認証情報は JWT や Cookie を使ったりする必要がある

まとめ

こんな感じにどの構成を取ろうともメリットデメリットがあり、正解は存在しないのかなと思っています。

個人的には各々のフレームワークの強いところを最大限に生かせる分離型が好きなんですが、そこまで大規模にならないような簡単なアプリだったり、もともとの Rails アプリを引き継がなきゃいけない理由とかがある場合は上に乗っける形でも全然アリだなとこの発表でいろいろ調べているうちに思えてはきました。

結局どの構成を取ろうともメリットとデメリットは存在するので最終的にはどのメリットが好きなのか、作成するアプリケーションやチームに合っているのかという部分だと思うので、どれが正解だと決めつけはせず柔軟に検討していかなければならないと思っています。

著者プロフィール

大谷 東彦

株式会社WESEEK / システムエンジニア

文系学科卒業後、プログラミングほぼ未経験で大手 SIer に入社。 インフラエンジニアを2年半ほど経験。

2015年11月WESEEKに入社し本格的に WEB プログラミングを始める。

前職の経験を活かしつつ、インフラからフロントエンドまで何でもこなす自称フルスタックエンジニア。

会社の slack に常駐し、進捗をひたすら垂れ流す日々を送る。最近はレビューにも力を入れ、後進の育成にも励んでいる。

株式会社WESEEKについて

株式会社WESEEKは、システム開発のプロフェッショナル集団です。

【現在の主な事業】

  1. 通信大手企業の業務フロー自動化プロジェクト
  2. ソーシャルゲームの受託開発
  3. 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発

GROWI

GROWIは、Markdown記法でページを記述できるオープンソースのWikiシステムです。

GROWI.cloud

GROWI.cloudはOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。

大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。

【主な特徴】

  • テキストも図表もどんどん書ける、強力な編集機能
  • チーム拡大に迅速に対応できる管理者向け機能を提供
  • 充実した機能・サポートでエンタープライズにも対応

【導入事例記事】

インターネットマルチフィード株式会社様
[https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite]

株式会社HIKKY(VR法人HIKKY)様
[https://growi.cloud/interviews/hikky:embed:cite]

WESEEK Tech Conference

WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会です。
月に2回ほど、WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。

次回は、9/2(木) 19:00~20:00『React でフォームをスマートに実装する』

初心者の方向けに、Form の基礎やReact でのフォームの使い方、そしてReactHookForm を使った便利なフォームの作り方を解説します!

現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております!
[https://weseek.connpass.com/event/222042/:embed:cite]
TECH PLAYはこちらから

一緒に働く仲間を募集しています

東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。

中途採用だけではなく、インターンシップも積極的に受け入れています!

詳しい募集要項は、弊社HPの採用ページからご確認ください。

関連記事

開発でのロックの重要性とORMのロック実現例〜楽観的ロックの利用例|フレームワーク O/R マッパー

開発でのロックの重要性とORMでのロックの実現例 |楽観的ロックの紹介

Puppeteerで定期的にスクリーンショットを取得する方法