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 完成
5.まとめ
できる限り簡単になるように、最低限の内容のみで行っています。
今回の内容は自分が日頃開発に使っている自作モジュールをけっこう持ち込んだ書き方なので、もっとマシな書き方は他にもあると思います。
そもそもRubyすらイジって3日目ぐらいの状態なので、あまりまともに受け取らないでください。
書いてみたらとりあえず動いたというようなレベルです。