方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

RailsとReactをモノレポで統合する方法:構成と実装例

rails-react-monorepo
この記事は約11分で読めます。

はじめに

モノレポ(Monorepo)は、「Monolithic Repository」の略で、複数のプロジェクトやコードベースを1つのリポジトリで管理する方法を指す。

通常は、1つのリポジトリに対して1つのプロジェクト(リポジトリ単位での管理)を行う「マルチレポ(Multi-repo)」とは対照的な手法。

モノレポでは、以下のような複数のアプリケーションやライブラリを1つのリポジトリで管理する

  • バックエンド(例: Rails)
  • フロントエンド(例: React、Vue.js)
  • 共通のユーティリティ(例: APIクライアント、ヘルパー関数)
  • サードパーティツールの統合

モノレポ構成を利用すれば、Railsをバックエンド、Reactをフロントエンドとして統合した効率的なプロジェクトを構築できる。以下では、RailsからReactにデータを渡し、動的なUIを構築する方法を具体的なコード例を用いて説明する。


モノレポ構成の概要

例えばRailsアプリケーションはapp/ディレクトリから始まり、フロントエンドはfrontend/packages以下で管理する。この構成では、Reactアプリケーションを@プレフィックスでモジュール化し、再利用性と管理性を向上させている。

ディレクトリ構成

Ruby
my-monorepo/
├── app/                     # Railsアプリケーション
│   ├── controllers/         # コントローラー
│   ├── models/              # モデル
│   ├── views/               # ビュー
│   │   ├── layouts/
│   │   │   └── react.html.erb   # React用共通レイアウト
│   │   └── tasks/
│   │       └── show.html.erb    # RailsからReactにデータを渡すテンプレート
├── frontend/                # フロントエンド(Reactなど)
│   ├── packages/            # フロントエンドのパッケージ
│   │   ├── @app/            # Reactアプリケーション
│   │   │   ├── src/
│   │   │   │   ├── index.tsx    # Reactエントリーポイント
│   │   │   │   ├── App.tsx      # Reactメインコンポーネント
│   │   │   │   ├── components/  # UIコンポーネント
│   │   │   │   │   └── TaskList.tsx  # タスクリストコンポーネント
│   │   │   └── package.json     # パッケージ設定
│   │   ├── @ui/              # 共通UIコンポーネント
│   │   ├── @helpers/         # ヘルパー関数
├── Gemfile                  # Railsの依存ライブラリ
├── package.json             # モノレポ全体のパッケージ設定

 

RailsからReactへのデータの流れ

RailsからReactにデータを渡し、Reactがそのデータを使って動的に描画する仕組みを記載

 

Railsでデータを準備

RailsのコントローラーでReactに渡すデータを準備する。

app/controllers/tasks_controller.rb

Ruby
class TasksController < ApplicationController
  def show
    @tasks = Task.where(user_id: current_user.id)
    render layout: 'react'
  end
end

 

Railsのビューでデータを埋め込む

react_dataヘルパーを使い、Reactに渡す初期データをJSON形式で埋め込む。

app/views/tasks/show.html.erb

Ruby
<%= react_data({
  today: Date.current.strftime('%Y-%m-%d'),
  tasks: @tasks.map do |task|
    {
      id: task.id,
      name: task.name,
      done: task.done,
      due_date: task.due_date.strftime('%Y-%m-%d')
    }
  end
}) %>

 

このコードがHTMLにレンダリングされると、以下のようなスクリプトタグが生成される

<script>タグ:

  • class="react-data": Reactデータを特定するためのクラス属性。
  • data属性: JSON形式でRailsから渡されたデータを格納。文字列形式のJSONとして埋め込まれている
Ruby
<script class="react-data" data='{
  "today": "2025-01-13",
  "tasks": [
    {
      "id": 1,
      "name": "Task A",
      "done": false,
      "due_date": "2025-01-15"
    },
    {
      "id": 2,
      "name": "Task B",
      "done": true,
      "due_date": "2025-01-20"
    }
  ]
}'></script>

 

 

Railsで埋め込まれたデータを取得するユーティリティ

Railsで生成されたHTMLには、react_dataヘルパーによってデータが埋め込まれている。このデータをReactで取得するために、ユーティリティ関数getReactDataを用意する。

frontend/packages/@app/src/utils/getReactData.ts

TypeScript
/**
 * Railsから渡されたデータを取得する関数
 * @param selector データが埋め込まれた要素のセレクタ(デフォルト: `.react-data`)
 * @param dataAttr データが格納されている属性名(デフォルト: `data`)
 */
const getReactData = (selector: string = '.react-data', dataAttr: string = 'data') => {
  const nodes = document.querySelectorAll(selector);
  return Array.from(nodes).reduce((mergedData, node) => {
    const data = node.getAttribute(dataAttr);
    return { ...mergedData, ...(data ? JSON.parse(data) : {}) };
  }, {});
};

export default getReactData;

 

作成したgetReactData関数を使うことで、Railsから送られてきたJSON形式のデータを、React側でJavaScriptのオブジェクトとして利用できる形に変換している。

TypeScript
const initialData = getReactData();

console.log(initialData);
// 出力:
// {
//   today: "2025-01-13",
//   tasks: [
//     { id: 1, name: "Task A", done: false, due_date: "2025-01-15" },
//     { id: 2, name: "Task B", done: true, due_date: "2025-01-20" }
//   ]
// }
JSON形式JavaScriptオブジェクト
データ交換用のテキスト形式実行可能なJavaScriptオブジェクト
キーは必ずダブルクォートで囲むキーはダブルクォート不要(文字列でも可)
例: {"key": "value"}例: { key: "value" }

 

Railsで生成されたデータをReactアプリケーションに渡す

Reactアプリケーションのエントリーポイントで、getReactDataを使ってデータを取得し、親コンポーネントに渡す。

frontend/packages/@app/src/index.tsx

TypeScript
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import getReactData from './utils/getReactData';
import App from './App';

// Railsから渡されたデータを取得
const initialData = getReactData();

const appElement = document.getElementById('app');

if (appElement) {
  createRoot(appElement).render(
    <StrictMode>
      <App {...initialData} />
    </StrictMode>
  );
}

 

Reactの親コンポーネントでデータを受け取る

親コンポーネントAppは、Railsから渡されたデータをpropsとして受け取り、必要に応じて子コンポーネントに渡す。

frontend/packages/@app/src/App.tsx

TypeScript
import React from 'react';
import TaskList from './components/TaskList';

interface Task {
  id: number;
  name: string;
  done: boolean;
  due_date: string;
}

interface AppProps {
  today: string;
  tasks: Task[];
}

const App: React.FC<AppProps> = ({ today, tasks }) => (
  <div>
    <h1>タスク一覧</h1>
    <p>今日の日付: {today}</p>
    <TaskList tasks={tasks} />
  </div>
);

export default App;

 

Reactの子コンポーネントでデータを描画する

親コンポーネントから渡されたデータを使い、タスクリストを描画する子コンポーネントを作成する。

frontend/packages/@app/src/components/TaskList.tsx

TypeScript
import React from 'react';

interface Task {
  id: number;
  name: string;
  done: boolean;
  due_date: string;
}

interface TaskListProps {
  tasks: Task[];
}

const TaskList: React.FC<TaskListProps> = ({ tasks }) => (
  <ul>
    {tasks.map((task) => (
      <li key={task.id}>
        <label>
          <input type="checkbox" checked={task.done} readOnly />
          {task.name}(締切: {task.due_date})
        </label>
      </li>
    ))}
  </ul>
);

export default TaskList;

 

 

まとめ

  1. Railsでデータを埋め込む
    • react_dataヘルパーでRailsのデータをJSON形式でHTMLに埋め込む。
  2. Reactエントリーポイントでデータを取得
    • getReactDataを使って埋め込まれたデータを取得し、<App />に渡す。
  3. 親コンポーネントから子コンポーネントにデータを渡す
    • 親コンポーネントがデータを受け取り、子コンポーネントにpropsとして伝播。
  4. Reactで動的なUIを構築
    • 取得したデータを基に、Reactが動的にUIをレンダリング。

RailsとReactを組み合わせることで、動的でインタラクティブなフロントエンドを実現可能。

タイトルとURLをコピーしました