メッセージ系のSNSを作ろうとしたのに掲示板になっていない？

# SNSと掲示板の違い
>エンジニアなら誰しも一回はLINEのようなSNSアプリを作りたいと思ったことはあるはずです


**SNSもどきの例**

![u8amv-lvx1w.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1980538/29b45b89-a1e0-c7c2-86b2-8ed257822b3a.gif)



<font color="Red">問題点</font>
-
- リアルタイムで更新されていないためコミュニケーションが図れない!!!


> Twitterのタイムラインで手動で更新する形、掲示板でスーパーリロードをかけて更新する場合とは大きく性質が異なる。画面開いたままのユーザーもいるため画面遷移時にAPIを叩いていたのではまともな会話にならない。


# 今回使うもの
[フロント用のテンプレート](https://github.com/kawabata324/template-react-typescript-vite)
React Typescript
[APIのテンプレート](https://github.com/kawabata324/template-rails6-api-docker)
Docker Ruby On Rails
`他の人のパクってエラーでた箇所修正しました`

よかったら使ってください　
Branch切って番号降ってるので進捗度合いもわかると思います

# 簡易な解決方法 (多分ダメ、最悪)

setIntervalを使って毎秒ごとに更新をかける

``` react
  setInterval(() => {
    connectTest();
  }, 1000);

  const connectTest = async () => {
    await axios
      .get('http://localhost:3000/test/tests/index')
      .then((res) => {
        setMessages(res.data.messages);
      })
      .catch((e) => console.log(e));
  };
```

**[結果]取り合えずリアルタイム更新は実現した**

<font color="Red">問題点</font>
-
- メッセージが更新されていなくてもAPIを叩く羽目になる。
- 早期リターンしたとしても負荷がかかる
- 再レンダリングがかかってたりする
- 大規模で同時接続数が多いアプリでこの方法は多分成り立たない
- 多少とはいえラグありそう

>APIを不必要に何回も叩くのは絶対良くない。これは初学者の僕でもわかる

*AJAXポーリング*というらしい

# 本題　WebSocketを使おう

## WebSocketとは

>HTTP通信は通常、ブラウザなどからの要求にサーバーが応答を返すという形でしか通信できない。
webSocketは、必要に応じてサーバーからクライアントに対して通信を行える。双方向通信と呼ばれる通信方法を実現するシステムのことで、リアルタイムチャットのチャット機能を作るにはとても便利。

こういうの見た方が早い

https://qiita.com/chihiro/items/9d280704c6eff8603389

https://www.freshvoice.net/knowledge/word/6323/

ちなみに自分でまとめたものもあるのでよかったら見てください

https://shrub-music-16e.notion.site/WebSocket-c3a187919fa54161b428c734bcb9ed56

## Action Cable

>Action Cableは、Railsのアプリケーションと同様の記述で、WebSocket通信という双方向の通信によるリアルタイム更新機能を実装できるフレームワークで、Rails5から実装されました。
Action Cableを利用することで、たとえばリアルタイムで更新されるチャット機能を実装することができます。

他の記事頼みの説明

https://qiita.com/yokoto/items/fb1e66844b70f52682dd

https://railsguides.jp/action_cable_overview.html

# 実践



## API(Rails)

## 環境構築
https://lamila.hatenablog.com/entry/2021/10/11/114521

config/environments/development.rb
ActionCableにあらゆるオリジンからの通信を許可するため、以下のコメントアウトを解除します

```ruby
  config.action_cable.disable_request_forgery_protection = true
```

config/environments/production.rb
development同様disable_request_forgery_protectionをtrueにします。
それに加えて、通信を許可するオリジンをallowed_request_originsに配列型式で追加します

```ruby
config.action_cable.allowed_request_origins = ['https://your-staging-domain', 'https://your-production-domain']
config.action_cable.disable_request_forgery_protection = true
```

/cableにサーバーをマウントするよう設定します。このパスがWebSocketのコネクションを開始するためのルートパスになります
``` ruby
  mount ActionCable.server => '/cable'
```

## コネクションの確立
app/channels/application_cable/connection.rb
本来ならここでコネクションを制限したりする？ uniqな値を振り分ける。おそらく解錠の時に必要だから

```ruby
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :chat_token

    def connect
      self.chat_token = Time.now.to_i
    end

  end
end
```

## チャネルの作成

```shell
$ docker-compose run web rails g channel Chat
```

app/channels/chat_channel.rb

```ruby
class ChatChannel < ApplicationCable::Channel
# チャネルがフロント側で作成された時に走るメソッド
  def subscribed
    stream_from "chat_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

# フロントから送られてきたデータを使ってテーブルに保存している。
# その後ブロードキャストをして（不特定多数のsubscriber)に対して保存したデータを送信している
  def speak(data)
    createdMessage = Message.create!({ body: data["message"] })
    ActionCable.server.broadcast("chat_channel", { message: createdMessage })
  end
end
```

## フロント(React)

src/App.tsx

``` ts
import React, { ChangeEvent, useEffect, useState } from 'react';
import './App.css';
import axios from 'axios';
import ActionCable from 'actioncable';

interface Message {
  id: number;
  body: string;
  created_at: string;
  updated_at: string;
}

function App() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputText, setInputText] = useState('');
  const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
  const channel = cable.subscriptions.create('ChatChannel', {
    // chanelとコネクトした時に走るメソッド
    connected: () => {
      console.log('コネクト成功');
    },
    // broadcastで不特定多数に送られたものを受け取る
    received: (data) => {
      // idが同じものだったら即リターンする
      const existingMessages = messages.filter((message) => {
        message.id === data.message.id;
      });
      if (existingMessages.length !== 0) return;
      console.log(data);
      setMessages([...messages, data.message]);
      setInputText('');
    },
  });

  useEffect(() => {
    fetchMessages();
  }, []);

  const fetchMessages = async () => {
    await axios
      .get('http://localhost:3000/test/tests/index')
      .then((res) => {
        setMessages(res.data.messages);
      })
      .catch((e) => console.log(e));
  };

  const changeInputText = (e: any) => {
    setInputText(e.target.value);
  };

  const clickSendMessage = async () => {
    // ここでchanelのメソッドを呼び出している。これによってspeakメソッドが発火する
    channel.perform('speak', {
      message: inputText,
    });
  };

  return (
    <div className="App">
      <h1>ChatRoom</h1>
      <ul>{messages !== [] ? messages.map((message) => <li key={message.id}>{message.body}</li>) : null}</ul>
      <label htmlFor="">Say Something</label>
      <input type="text" value={inputText} onChange={changeInputText} />
      <button onClick={clickSendMessage}>送信</button>
    </div>
  );
}

export default App;
```
# 完成
![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1980538/5c14e512-c950-4728-2c5a-099afa39a6ad.gif)


## 最後に
websocketを使っての双方向の通信ができるようになった。
しかし、聞きなれない単語が多くどうしても理解が浅い部分は多い。安全にデータを取り扱ったり、気持ちの良いコードとはいえないのでもっと学習してより深く学んでいきたい

## 参考

この記事を作るにあたって以下の記事を参考にしました！

https://lamila.hatenablog.com/entry/2021/10/11/114521
https://railsguides.jp/action_cable_overview.html