LoginSignup
14
14

More than 3 years have passed since last update.

RailsをAPIサーバにしてReact+TypeScriptと連携させる

Last updated at Posted at 2019-10-17

RailsをAPIサーバにしてReact+TypeScriptと連携させる

こちらでも同じものを書いています
https://ttis.croud.jp/?uuid=97316adb-4153-45a2-8ea4-70beb76f2be6

1.前提として

 私は日頃はサーバ側をNode.js+TypeScriptで構築しているので、その感覚をRailsに持ってきています。ということでRailsで書くべき流儀は全て無視というか、そもそも知りません

2.環境設定

  • yarnのインストール(Railsはnpmのパッケージ管理をyarn前提としているようなので)
npm -g i yarn
  • ベースとなるRailsプロジェクトの作成(名前は何でも良い)
rails new bbs-app
  • ディレクトリの移動(そもそも開発環境で開くのなら不要)
cd bbs-app
  • RailsでReactを使うためのパッケージをインストール
yarn --dev add setup-template-rails-react

このパッケージをインストールすることによって、プロジェクトが以下のような追加構成になります

root/  
 ├ app/
 │ └ assets/
 │  └ javascripts/ (トランスコンパイル出力ディレクトリ)
 │   ├ bundle.js
 │   └ bundle.map
 └ front/ (フロントエンド用ディレクトリ)  
  ├ src/ (JavaScript/TypeScript用ディレクトリ)  
  │ ├ .eslintrc.json  
  │ ├ index.tsx  (サンプルReactソース)
  │ └ tsconfig.json  
  └ webpack.config.js  
  • フロントエンドがらみの追加モジュールのインストール
    Reduxでの状態管理やAjaxで通信するときに後で使います
yarn --dev add react-redux @types/react-redux @jswf/redux-module @jswf/adapter

3.Railsの基本設定

3.1 トップページの差し替えてReactを動かす状態を作る

コントローラを追加

rails generate controller root index

/app/views/root/index.html.erb の内容をbundle.jsを呼び出すように直す

/app/views/root/index.html.erb
<div id="root"></div>
<%= javascript_include_tag 'bundle.js' %>

/config/routes.rb の内容を修正する

/config/routes.rb
Rails.application.routes.draw do
  root 'root#index'
end

/config/initializers/assets.rb に、以下の設定を追加
(これを記述したらrailsを再起動)

/config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( bundle.js )

3.2 ReactのビルドからRailsの実行まで

  • Reactのビルド(自動ビルドの場合はnpm run watch)
npm run build
  • Railsの起動
npm start
  • 確認
    http://localhost:3000/ にアクセスして「こんにちは世界!」と表示されていることを確認する
    front/src/index.tsxの中を編集すると、内容を書き換えることが出来る リアルタイムにリビルドを行いたい場合はnpm run watchを実行する

4.SPAの掲示板を作る

4.1 Rails側(バックエンド)

  • モデルとコントローラの作成
rails generate model messages name:string body:string
rails db:migrate
rails generate controller messages
  • コントローラにJSON形式の送受信機能を付ける
    データが送られてきたら保存し、受信の有無に関係なくDBの内容を全てJSONで返却
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  protect_from_forgery
  def index
    begin
      p = JSON.parse(request.body.read, { symbolize_names: true })
      if p[:name] && p[:body]
        message = Message.new
        message.name = p[:name]
        message.body = p[:body]
        message.save
      end
    rescue StandardError
    end
    messages = Message.all
    render json: messages
  end
end
  • ルーティングの修正
/config/routes.rb
Rails.application.routes.draw do
  root 'root#index'
  match 'messages', to: 'messages#index', via: %i[get post]
end

4.2 React側(フロントエンド)

  • データ管理用モジュールの作成
    Reduxを簡単に扱うため@jswf/redux-moduleを使用しています
front/src/MessageModule.ts
import { ReduxModule } from "@jswf/redux-module";
import { Adapter } from "@jswf/adapter";

//メッセージの構造
interface Message {
  id: number;
  name: string;
  body: string;
  created_at: Date;
  updated_at: Date;
}
//Storeで使う構造
interface State {
  messages: Message[];
}
//データモジュールの定義
export class MessageModule extends ReduxModule<State> {
  protected static defaultState: State = {
    messages: []
  };
  public write(message: { name: string; body: string }) {
    Adapter.sendJsonAsync("messages", message).then(e => {
      if (e instanceof Array) this.setState({ messages: e as Message[] });
    });
  }
  public load() {
    Adapter.sendJsonAsync("messages").then(e => {
      if (e instanceof Array) this.setState({ messages: e as Message[] });
    });
  }
}
  • 送信フォームコンポーネント
front/src/MessageForm.tsx
import { useRef } from "react";
import React from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";

export function MessageForm() {
  const messageModule = useModule(MessageModule, undefined, true);
  const message = useRef({ name: "", body: "" }).current;
  return (
    <div>
      <div>
        <button
          onClick={() => {
            messageModule.write(message);
          }}
        >
          送信
        </button>
      </div>
      <div>
        <label>
          名前
          <br />
          <input onChange={e => (message.name = e.target.value)} />
        </label>
      </div>
      <div>
        <label>
          メッセージ
          <br />
          <input onChange={e => (message.body = e.target.value)} />
        </label>
      </div>
    </div>
  );
}
  • 掲示板表示コンポーネント
front/src/MessageList.tsx
import React, { useEffect } from "react";
import { useModule } from "@jswf/redux-module";
import { MessageModule } from "./MessageModule";

export function MessageList() {
  const messageModule = useModule(MessageModule);
  //初回のみメッセージを読み込む
  useEffect(() => {
    messageModule.load();
  }, []);
  //メッセージをStoreから取得
  const messages = messageModule.getState("messages")!;
  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>
          <hr />
          <div>
            {msg.id}:[{msg.name}]{msg.created_at}
          </div>
          <div>{msg.body}</div>
        </div>
      ))}
    </div>
  );
}
  • トップコンポーネント
front/src/index.tsx
import React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { ModuleReducer } from "@jswf/redux-module";
import { MessageForm } from "./MessageForm";
import { MessageList } from "./MessageList";

function App() {
  return (
    <>
      <MessageForm />
      <MessageList />
    </>
  );
}

const store = createStore(ModuleReducer);
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root") as HTMLElement
);

4.3 完成

image.png

5.まとめ

 できる限り簡単になるように、最低限の内容のみで行っています。

 今回の内容は自分が日頃開発に使っている自作モジュールをけっこう持ち込んだ書き方なので、もっとマシな書き方は他にもあると思います。
 そもそもRubyすらイジって3日目ぐらいの状態なので、あまりまともに受け取らないでください。
 書いてみたらとりあえず動いたというようなレベルです。

14
14
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
14
14