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

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

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

## １．前提として

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

## ２．環境設定

- yarnのインストール(Railsはnpmのパッケージ管理をyarn前提としているようなので)

```sh
npm -g i yarn
```

- ベースとなるRailsプロジェクトの作成(名前は何でも良い)

```sh
rails new bbs-app
```

- ディレクトリの移動(そもそも開発環境で開くのなら不要)

```sh
cd bbs-app
```

- RailsでReactを使うためのパッケージをインストール

```
yarn --dev add setup-template-rails-react
```

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

```txt
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
```

## ３．Railsの基本設定


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

コントローラを追加

```sh
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を実行する


## ４．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[] });
    });
  }
}
```

- 送信フォームコンポーネント

```tsx: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>
  );
}
```

- 掲示板表示コンポーネント  

```tsx: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>
  );
}
```

- トップコンポーネント

```tsx: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267629/999e72b9-202a-970c-9ca7-ef9b01f57cf6.png)


## ５．まとめ

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