MadoiとReactでチャットアプリを作ってみよう

# はじめに

こんにちは。[京都情報大学院大学](https://www.kcg.edu/)、未来環境ラボの中口です。この記事では、Madoi と React と組み合わせて、簡単なチャットアプリケーションを作る方法を紹介します。Madoi(参考: [WebSocketメッセージングサーバ"Madoi"をASL2で公開しました](https://qiita.com/takawitter/items/251bc327870c223529d5))は、ネットワーク共有機能を利用したウェブアプリを開発するためのオープンソースのメッセージングサーバ + クライアントライブラリです。


# まとめ

* Madoi と React を組み合わせる[ヘルパ](https://github.com/kcg-edu-future-lab/madoi-sample-ts-react-showcase/blob/main/src/madoi/reactHelpers.ts)が用意されている
* useMadoiState フックで共有状態を扱える
* useMadoiObject フックで共有オブジェクトを扱える
* React らしい記述でチャットなどの共有アプリを簡単に作れる！
* 最終的に出来上がったチャット(vite + ts + react)のソース一式は[ここ](https://github.com/kcg-edu-future-lab/madoi-react-simplechat)


# Madoiの仕組みとReact

### Madoiにおける共有の仕組み

Madoi は、Madoi Client(TypeScript/JavaScript ライブラリ) と Madoi Server(メッセージング + 状態管理サーバ)で構成されています。メッセージ送受信機能だけを利用することもできますが、メソッド実行の共有によるオブジェクト状態同期機能も提供しています。後者では、共有対象のメソッドが実行されようとした際に、Madoi Clientがその実行を横取りし、Madoi Server に通知を送ります。Madoi Server は参加しているクライアント全てに通知を配信し、通知を受け取ったクライアントはメソッドを実行します。これにより、あるクライアントでメソッドが実行されようとすると、全てのクライアントで同じメソッドが実行されます(後述する`useMadoiState` がメッセージ送受信のみを使用し、`useMadoiObject`がメソッド実行の共有を利用しています)。更新された状態を表示することにより、あるユーザの操作が他のユーザに共有されます。


```mermaid
flowchart TD;
  MadoiServer[Madoi Server]
  subgraph ClientB
    direction TB
    UIB[UI]
    メソッドB[オブジェクト]
    MadoiB[Madoi Client]
  end
  subgraph ClientA
    direction TB;
    UIA[UI]
    メソッドA[オブジェクト]
    MadoiA[Madoi Client]
  end
  
  UIA-->|"1.メソッドを実行しようとする"|メソッドA
  メソッドA-->|2.横取り|MadoiA
  MadoiA-->|3.通知|MadoiServer
  MadoiServer-->|4.通知|MadoiA
  MadoiServer-->|4'.通知|MadoiB
  MadoiA-->|5.メソッド実行|メソッドA
  MadoiB-->|5'.メソッド実行|メソッドB
  メソッドA-->|6.表示|UIA
  メソッドB-->|6'.表示|UIB
```

### React

React は、状態を描画することに特化したUIフレームワークです。UIを描画する(=HTMLを生成する)関数を用意し、内部で useState フックを実行すると、状態と変更関数が返されます。状態はHTMLの描画に使用し、変更関数はボタンクリックなどのイベント発生時に実行します。変更関数を実行すると、UIを描画する関数が再度呼ばれ、その際 useState からは変更後の状態が返され、状態を反映したHTMLが生成されて画面が更新されます。

以下は入力された文字列をリストに追加するプログラムの例です。

```main.tsx
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
```

```App.tsx
export default function App() {
  const [logs, setLogs] = useState<string[]>([]);
  const input = useRef<HTMLInputElement>(null!);
  const onSubmit: FormEventHandler = e=>{
    e.preventDefault();
    setLogs(logs=>[...logs, input.current.value]);
  };
  return <>
    <form onSubmit={onSubmit}>
      <input ref={input} />
      <button>送信</button>
    </form>
    <div>
      {logs.map(
        (log, index)=><div key={index}>{log}</div>        
      )}
    </div>
  </>;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4003/149cf703-2eab-2182-e90a-6c6b2368e44b.png)

`useState`で文字列の配列を状態として宣言し、状態`logs`と変更関数`setLogs`を受け取り、コードの後半では`logs`の内容をdivタグに変換しています。次に`useRef`でテキストボックスを操作するための変数`input`を宣言し、イベントハンドラ`onSubmit`内で状態`logs`と`input`の内容を連結して、変更関数`setLogs`に渡しています。`setLogs`実行後、Reactにより再度`App()`が実行され、渡された内容が`useState`の戻り値の`logs`に格納されて返され、表示に反映されます。

# useMadoiStateによる状態共有機能の追加

前節で示したプログラムを、Madoiを使って共有アプリに変更してみましょう。

まず、[Madoiのプロジェクトページ](https://github.com/kcg-edu-future-lab/madoi)を参考に、Madoiサーバを起動して下さい(Dockerが必要です)。

次に、[Madoiクライアント](https://github.com/kcg-edu-future-lab/madoi-react-simplechat/blob/main/src/madoi/madoi.ts)と[ヘルパー](https://github.com/kcg-edu-future-lab/madoi-react-simplechat/blob/main/src/madoi/reactHelpers.ts)をダウンロードしてプロジェクトに加えて下さい(最終的なソースコードは[こちら](https://github.com/kcg-edu-future-lab/madoi-react-simplechat))。

次に、`main.tsx`で、Madoiクライアントを使ってサーバに接続します。

```main.tsx
const roomId = "madoiChat_2rnieg"; // 追加。任意の文字列
const apikey = "ahfuTep6ooDi7Oa4"; // 追加。apikey
export const MadoiContext = createContext(new Madoi(  // 追加
    `ws://localhost:8080/madoi/rooms/${roomId}`,
    apikey));

createRoot(document.getElementById('root')!).render(
...(以降は変更無し)...
```

この例では、React の context(MadoiContext) として Madoi クライアントを生成しています。接続先は先ほど起動した、localhost の Madoi Server です。

次に、`App.tsx`で、MadoiContext から Madoi Client を取得して文字列のリストを共有状態にします。

```(共有状態版)App.tsx
export default function App() {
  const madoi = useContext(MadoiContext);  // 追加
  const [logs, setLogs] = useMadoiState<string[]>(madoi, []);  // 変更
  const input = useRef<HTMLInputElement>(null!);
...(以降は変更無し)...
```

App 関数の冒頭で useContext を使って madoi(`main.tsx`で作成したもの) を取得し、次の行の `useState` を `useMadoiState` に変更して madoi を渡すようにしました。Madoi Client の利用に必要な変更はこれだけです。

これだけの変更で、アプリケーションがネットワーク共有に対応できました。

# 問題点とuseMadoiObject

前節では、文字列のリストを共有の状態として扱うことで、チャットアプリのような振る舞いを実現しました。しかし、複数のクライアントで同時に状態の更新が行われると、タイミング次第で、追加した内容が失われてしまうことがあります。

```mermaid
sequenceDiagram
  participant aa as Client A
  participant s as Madoi Server
  participant ab as Client B
  aa->>aa: "hello"を追加
  aa->>s: ["hello"]
  ab->>ab: "world"を追加
  ab->>s: ["world"]
  s->>aa: ["hello"]
  aa->>aa: ["hello"]を表示
  s->>ab: ["hello"]
  ab->>ab: ["hello"]を表示
  s->>aa: ["world"]
  aa->>aa: ["world"]を表示
  s->>ab: ["world"]
  ab->>ab: ["world"]を表示
```

Madoi Server は、送られてきた通知を、受け取った順番に全てのクライアントに転送します。そのため、状態そのものを同時に送り合い、受け取った状態をそのまま表示すると、最後に送った状態だけが表示されることになります。これは、ネットワークアプリケーションにおいて最も深刻な、クライアント毎に保持している情報が異なる不整合な状態よりはマシですが、チャットアプリケーションとしては想定した動作ではありません。

求める動作を実現するには、状態とそれに対する操作をクラスにまとめ、操作を共有するよう変更する必要があります。まず、次のようなクラスを用意します。

```Logs.ts
@ShareClass({className: "Logs"})
export class Logs{
  private logs: string[] = [];
  @Share()
  add(log: string){
    this.logs = [...this.logs, log];
  }
  @GetState()
  getLogs(){
    return this.logs;
  }
  @SetState()
  setLogs(logs: string[]){
    this.logs = logs;
  }
}
```

`@ShareClass`, `@Share`, `@GetState`, `@SetState`は、Madoi Client が用意しているデコレータです。それぞれ共有オブジェクトを定義するクラス、状態変更を起こすメソッド、状態取得メソッド、状態設定メソッドに付与します。Madoi Client は、このデコレータを手がかりに、メソッド実行の共有や状態の取得設定を行います。共有オブジェクトの状態自体は管理の対象にはせず、常にデコレータが指定されたメソッドを通じてオブジェクトを操作します。

次に、`App.tsx`を、上記のクラスを使うように書き換えます。

```(共有オブジェクト版)App.tsx
export default function App() {
  const madoi = useContext(MadoiContext);
  const logs = useMadoiObject(madoi, new Logs()); // 変更
  const input = useRef<HTMLInputElement>(null!);
  const onSubmit: FormEventHandler = e=>{
    e.preventDefault();
    logs.add(input.current.value); // 変更
  };
  return <>
    <form onSubmit={onSubmit}>
      <input ref={input} />
      <button>送信</button>
    </form>
    <div>
      {logs.getLogs().map(   // 変更
        (log, index)=><div key={index}>{log}</div>        
      )}
    </div>
  </>;
}
```

`useMadoiState` の代わりに `useMadoiObject` を呼び出すようにし、文字列の配列を状態として扱っていたところを、Logs クラスのオブジェクトに変更しました。これにより、状態ではなく操作(`addLog` メソッドの呼び出し)が共有されるようになり、複数のクライアントで同時に文字列の追加が行われても、文字列が失われることは無くなりました。


# おわりに

この記事では、オープンソースメッセージングサーバ Madoi と React を使って、非常に単純なチャットアプリケーションを作成しました。

Madoi は研究の成果展開の一環で公開している基盤システムです。ネットワークプログラミングを極限まで単純化し、少ないコード量で共有アプリケーションが開発できます。

実験的なソフトウェアであり、商用を見据えたテストは行っていませんので、as is でご利用ください。リクエストやバグ報告などあれば[Madoiのリポジトリ](https://github.com/kcg-edu-future-lab/madoi)までお寄せください！