A React library for Distributed Sharing Platform: Madoi.
分散共有基盤MadoiのReact用クライアントライブラリ。
npm i madoi-client madoi-client-reactconst MadoiContext = createContext({
madoi: new Madoi("wss://host/madoi-server/rooms/room000", "MADOI_API_KEY")
});最初にMadoiクライアントを作成してください。引数はMadoiサーバのURLとAPIキーです。あらかじめ、Madoiサーバを起動しておく必要があります。
function StateExample(){
const ctx = useContext(MadoiContext);
const [msg, setMsg] = useSharedState(ctx.madoi, "hello");
const onClick: MouseEventHandler = ()=>{
// setメソッドに渡された状態は、全てのアプリケーションに共有される
setMsg(v => v === "hello" ? "world" : "hello");
}
return <div>
<h4>madoi state</h4>
<button onClick={onClick}>{msg}</button>
</div>;
}useSharedState は、useState の分散共有版です。
同じMadoiサーバ上のルームに参加しているアプリケーション間で、状態の同期が行われます。
上記の例では、一つのアプリケーション内で setMsg が実行されると、新しい状態が全てのアプリケーションに通知され、既存の状態と異なる場合はReactのレンダリングが行われます。
クリックハンドラで setMsg が呼び出されているので、ボタンがクリックされるたびに全てのアプリケーションでラベルが変更されます。
useSharedState は、手軽に利用できる反面、毎回状態を送信するため、サイズが大きい場合(例えば画像の場合)は通信コストが増大します。
また、利用者間で状態変更操作が競合しやすいという欠点があります(例えば画像に対して2人が同時に描画した場合、後で描画した方の状態で上書きされてしまう)。
次に紹介する useSharedReducer および useMadoiModel は、状態の更新操作を個別に共有するため、競合の回避が行いやすい方式です。
export default function App() {
const ctx = useContext(MadoiContext);
function reducer(prevState: number, action: "inc" | "dec"): number{
switch(action){
case "inc": return prevState + 1;
case "dec": return prevState - 1;
}
}
const [count, dispatch] = useSharedReducer(ctx.madoi, reducer, 0);
return <div>
<button onClick={() => dispatch("inc")}>inc</button>
<button onClick={() => dispatch("dec")}>dec</button>
Count is {count}
</div>;
}useSharedReducer は、useReducer の分散共有版です。
useReducerは状態とその変更操作を扱うReactのフックで、dispatch 関数を実行すると、その引数と現在の状態が
reducer 関数に渡され、reducer 関数が返した値と元の状態が異なれば、レンダリングが行われます。
上記の例では、incボタンをクリックするとカウントが増加し、decボタンをクリックすると減少します。
新しくページを開くと、最新のカウント値が送信され、表示されます。
useShareReducer が返す dispatch 関数を実行すると、同じMadoiサーバ上のルームに参加している全てのアプリケーションに reducer の実行要求が送信され、受信後に reducer とレンダリング処理がそれぞれのアプリケーション内で実行されます。
同時に dispatch が複数回実行されても、全てのアプリケーションで実行順が同じになるよう実装されているため、reducer が純粋関数として実装されている限り、全てのアプリケーションで同じ結果になります。
/*
* チャットログを保持するモデルクラス
*/
class ChatLogs{
private logs: string[] = [];
// ルーム内の他のアプリケーションでも実行するメソッドには@Distributedデコレータを追加する
// 状態を変更するメソッドには@ChangeStateデコレータを追加する
@Distributed()
@ChangeState()
addLog(text: string){
this.logs = [...this.logs, text];
}
// 状態を取得するメソッドには@GetStateデコレータを追加する
@GetState()
getLogs(){
return this.logs;
}
// 状態を設定するメソッドには@SetStateデコレータを追加する
@SetState()
setLogs(logs: string[]){
this.logs = logs;
}
}
function ModelExample(){
const ctx = useContext(MadoiContext);
const inputRef = useRef<HTMLInputElement>(null!);
const logs = useMadoiModel(ctx.madoi, ()=>new ChatLogs());
const onSubmit: FormEventHandler = e=>{
e.preventDefault();
// @Distributedが指定されたメソッドは、他の全てのアプリケーションでも実行される
logs.addLog(inputRef.current.value);
}
return <div>
<h4>madoi model</h4>
<form onSubmit={onSubmit}>
<input ref={inputRef} type="text"></input>
</form>
<div>
{logs.getLogs().map(l => <div>{l}</div>)}
</div>
</div>;
}useMadoiModel は、状態を保持し、かつ状態への変更操作を持っているオブジェクトを登録します。
登録したオブジェクトの@Distributed()が指定されたメソッドが実行されると、他の全てのアプリケーションでも実行されます。
より厳密には、実行がMadoiライブラリにより横取りされ、引数とともにサーバへ送信され、全てのアプリケーションに送信され、
アプリケーションで受信した際にメソッドが実行されます。
複数のクライアントで同時にメソッドが実行された場合も、サーバに届いた順にアプリケーションへ送信されるため、全てのアプリケーションでメソッドの実行順は同じになります。
@ChangeState()が指定されたメソッドが実行されると、状態取得メソッド(@GetStateが指定されたメソッド)が実行され、状態が取得されます。
次に、取得した状態と、実行前の状態との比較が行われ、変更されていれば、Reactのレンダリングが行われます。
また、状態はMadoiサーバにも送信され、アプリケーションが新しく参加してくれば、そのアプリケーションに通知されます。
これにより、チャットログの追加や画像への描画など、異なるアプリケーションで同時に起こりうる操作を、 取りこぼすことなくオブジェクトに反映できます。
残念ながら TypeScript のデコレータは、すべてのビルドツールでデフォルトでサポートされているわけではなく、デコレータを使用したコードを正しくビルドし実行するために追加の設定が必要な場合があります。以下に、動作を確認したビルドツールとその設定例を示します。
vite(8.0)とplugin-reactを使用する場合、以下の設定が必要です。
import { defineConfig } from 'vite'
import react from "@vitejs/plugin-react"
import babel from "@rolldown/plugin-babel"
// https://vite.dev/config/
export default defineConfig({
plugins: [
babel({
presets: [{
preset: () => ({ plugins: [["@babel/plugin-proposal-decorators", { version: "legacy" }]] }),
rolldown: { filter: { code: "@" } },
}],
}),
react()
]
})plugin-react-swcを使用する場合は以下の設定が必要です。
import { defineConfig } from 'vite'
import react from "@vitejs/plugin-react-swc"
// https://vite.dev/config/
export default defineConfig({
plugins: [react({
useAtYourOwnRisk_mutateSwcOptions(options) {
options.jsc!.parser!.decorators = true
options.jsc!.transform!.decoratorVersion = 'lagacy'
},
})]
})以下のバージョンで動作を確認しています。
-
vite 7.1.7
- @vitejs/plugin-react-swc 4.1.0
- typescript 6.0.3
-
vite 8.0.16
- @vitejs/plugin-react-swc 4.3.1
- typescript 6.0.3
madoi-client-reactにより、MadoiとReactを組み合わせ、分散共有アプリを宣言的な記述で設計、実装できます。
より具体的なアプリケーション開発例は、ビデオ会議付きコラボレーションツール Presenceを参照ください。