CRDT × Zustand:Y.js/Automerge 連携でリアルタイム共同編集を設計
Google Docs のような複数人で同時にドキュメントを編集できる機能を、自分のアプリケーションにも実装したいと思ったことはありませんか?リアルタイム共同編集は、今や多くの Web アプリケーションで求められる重要な機能となっています。
本記事では、React の軽量状態管理ライブラリ Zustand と、競合解決を自動化する CRDT(Conflict-free Replicated Data Type) を組み合わせて、リアルタイム共同編集を実現する設計手法をご紹介します。特に、実績のある CRDT ライブラリである Y.js と Automerge を用いた実装パターンを、コード例とともに詳しく解説していきますね。
背景
CRDT とは
CRDT(Conflict-free Replicated Data Type)は、分散システムにおいてデータの競合を自動的に解決するデータ構造です。複数のユーザーが同時に同じデータを編集しても、特別な調整なしに最終的に一貫性のある状態に収束します。
従来の楽観的ロックや悲観的ロックといった競合制御と異なり、CRDT は数学的に証明された交換可能性(Commutativity)を持ちます。つまり、操作の適用順序が異なっても、最終的な結果は同じになるのです。
Zustand の特徴
Zustand は React 向けの状態管理ライブラリで、Redux や Recoil と比べて非常にシンプルな API を提供しています。以下のような特徴があります。
- Provider ラッパーが不要
- ボイラープレートコードが少ない
- TypeScript との相性が良い
- ミドルウェアによる拡張性
- 軽量(約 1KB)
この軽量さと柔軟性が、CRDT ライブラリとの連携に適しているのです。
Y.js の概要
Y.js は、最も成熟した JavaScript/TypeScript 用の CRDT ライブラリの一つです。以下の強みがあります。
- 高いパフォーマンス
- 豊富なデータ型(Text、Array、Map など)
- WebSocket、WebRTC などの通信プロトコルに対応
- Quill、Monaco Editor などのエディタとの統合
特にテキスト編集における CRDT 実装は非常に洗練されており、Google Docs のような体験を実現できます。
Automerge の特徴
Automerge は、JSON ライクなデータ構造を扱える CRDT ライブラリです。以下の特徴を持っています。
- 自動的な競合解決
- 完全な編集履歴の保持
- オフライン対応
- イミュータブルなデータ構造
Automerge は、ドキュメント全体を JSON として扱えるため、複雑なアプリケーション状態の同期に適しています。
CRDT と状態管理の関係性
以下の図は、CRDT と状態管理ライブラリ(Zustand)がどのように連携して、リアルタイム共同編集を実現するかを示しています。
mermaidflowchart TB
user1["ユーザー A"] --> ui1["React UI<br/>コンポーネント"]
user2["ユーザー B"] --> ui2["React UI<br/>コンポーネント"]
ui1 --> zustand1["Zustand Store"]
ui2 --> zustand2["Zustand Store"]
zustand1 --> crdt1["CRDT ドキュメント<br/>(Y.js/Automerge)"]
zustand2 --> crdt2["CRDT ドキュメント<br/>(Y.js/Automerge)"]
crdt1 <-->|同期| network["ネットワーク<br/>(WebSocket/WebRTC)"]
crdt2 <-->|同期| network
crdt1 -->|変更通知| zustand1
crdt2 -->|変更通知| zustand2
この図から分かるように、各ユーザーのクライアントは独立した CRDT ドキュメントを持ち、ネットワーク経由で変更を同期します。Zustand は React UI と CRDT ドキュメントの橋渡し役を担います。
図のポイント:
- 各クライアントが独自の CRDT ドキュメントを保持
- Zustand が UI と CRDT の間のインターフェースとなる
- ネットワーク経由で CRDT の変更が自動的に同期される
課題
従来の状態管理における競合問題
通常の状態管理ライブラリ(Redux、Recoil、Zustand など)は、シングルクライアントでの状態管理には優れていますが、複数クライアント間でのリアルタイム同期には対応していません。
例えば、以下のような問題が発生します。
問題 1:Last Write Wins(最後の書き込みが勝つ)
ユーザー A とユーザー B が同時にテキストを編集した場合、後から適用された変更が前の変更を上書きしてしまいます。これでは、片方のユーザーの編集内容が失われてしまいますね。
問題 2:バージョン競合
サーバー側でバージョン管理を行う場合、クライアント間のバージョンの不一致により、更新が拒否されることがあります。これではユーザー体験が損なわれます。
問題 3:ネットワーク遅延時の不整合
ネットワーク遅延がある環境では、各クライアントが異なる状態を表示し続ける可能性があります。
リアルタイム同期の複雑性
リアルタイム同期を実装するには、以下のような複雑な課題があります。
mermaidstateDiagram-v2
[*] --> LocalEdit: ユーザーが編集
LocalEdit --> Validation: 検証
Validation --> SendToServer: サーバーへ送信
SendToServer --> WaitResponse: 応答待ち
WaitResponse --> Conflict: 競合検出
WaitResponse --> Success: 成功
Conflict --> Merge: マージ処理
Merge --> ApplyChanges: 変更適用
Success --> ApplyChanges
ApplyChanges --> [*]
WaitResponse --> NetworkError: ネットワークエラー
NetworkError --> Retry: リトライ
Retry --> SendToServer
従来のアプローチで必要な処理:
- 競合検出ロジックの実装
- マージアルゴリズムの設計
- ロールバック・リトライ機構
- ネットワークエラー処理
これらを一から実装するのは、非常に困難で時間がかかります。
Zustand 単体での限界
Zustand は優れた状態管理ライブラリですが、以下の点で限界があります。
| # | 項目 | Zustand 単体 | CRDT との連携 |
|---|---|---|---|
| 1 | 競合解決 | 手動実装が必要 | 自動解決 |
| 2 | オフライン対応 | 複雑な実装 | 標準機能 |
| 3 | 操作履歴 | 別途実装 | 組み込み済み |
| 4 | ネットワーク同期 | カスタムロジック | プロトコル提供 |
| 5 | タイムトラベル | 自前実装 | CRDT が対応 |
このように、Zustand だけではリアルタイム共同編集に必要な機能が不足しています。CRDT との連携により、これらの課題を解決できるのです。
型安全性の維持
TypeScript を使用する場合、CRDT ライブラリと Zustand の間で型の整合性を保つことが課題となります。CRDT ドキュメントの構造と、Zustand の state 型が一致していないと、実行時エラーが発生する可能性があります。
解決策
CRDT と Zustand を組み合わせる設計思想
CRDT と Zustand を組み合わせることで、以下のような役割分担が可能になります。
Zustand の役割:
- React コンポーネントへの状態提供
- ローカルでの高速な状態更新
- TypeScript による型安全性の保証
- ミドルウェアによる拡張機能(ロギング、永続化など)
CRDT の役割:
- 競合の自動解決
- ネットワーク経由の同期
- 操作履歴の管理
- オフライン対応
この組み合わせにより、開発者はシンプルな Zustand の API を使いながら、裏側で CRDT が複雑な同期処理を担当するという、美しいアーキテクチャが実現できます。
アーキテクチャパターン
以下の図は、推奨するアーキテクチャパターンを示しています。
mermaidflowchart TD
subgraph Client ["クライアント側"]
ReactUI["React UI<br/>コンポーネント"]
ZustandStore["Zustand Store<br/>(Proxy Layer)"]
CRDTDoc["CRDT ドキュメント<br/>(Source of Truth)"]
ReactUI -->|read/subscribe| ZustandStore
ReactUI -->|action| ZustandStore
ZustandStore -->|update| CRDTDoc
CRDTDoc -->|observe| ZustandStore
end
subgraph Network ["ネットワーク層"]
Provider["Sync Provider<br/>(WebSocket/WebRTC)"]
end
subgraph Server ["サーバー側"]
SyncServer["同期サーバー"]
Persistence["永続化層<br/>(オプション)"]
SyncServer --> Persistence
end
CRDTDoc <-->|sync| Provider
Provider <-->|sync| SyncServer
設計のポイント:
- CRDT ドキュメントを「信頼できる唯一の情報源(Source of Truth)」とする
- Zustand Store は CRDT への Proxy として機能
- React UI は Zustand を通じて間接的に CRDT を操作
双方向バインディングの実装戦略
CRDT ドキュメントと Zustand Store の双方向バインディングを実現するには、以下の戦略が有効です。
CRDT → Zustand の更新: CRDT ドキュメントの変更イベントを監視し、Zustand の state を更新します。これにより、他のユーザーの変更が自動的に React UI に反映されます。
Zustand → CRDT の更新: Zustand の action 内で CRDT ドキュメントを更新します。この時、Zustand の state も即座に更新することで、楽観的 UI 更新を実現します。
無限ループの防止: 双方向バインディングでは、更新が無限にループしないよう、フラグやバージョン管理を用いた制御が必要です。
ミドルウェアパターンの活用
Zustand のミドルウェア機能を活用して、CRDT との連携ロジックをカプセル化できます。これにより、アプリケーションコードから CRDT の詳細を隠蔽し、シンプルな API を提供できますね。
mermaidflowchart LR
App["アプリケーション<br/>コード"] --> StoreAPI["Zustand<br/>Store API"]
StoreAPI --> Middleware["CRDT<br/>ミドルウェア"]
Middleware --> BaseStore["Base Store"]
Middleware <--> CRDTLib["CRDT<br/>ライブラリ"]
ミドルウェアが提供する機能:
- CRDT ドキュメントの初期化
- 同期プロバイダーの設定
- 変更イベントのハンドリング
- エラーハンドリング
具体例
Y.js と Zustand の連携実装
ここからは、実際のコード例を用いて、Y.js と Zustand を連携させる方法を解説します。
インストールとセットアップ
まず、必要なパッケージをインストールします。
bashyarn add zustand yjs y-websocket
yarn add -D @types/yjs
これらのパッケージにより、Zustand の状態管理機能と、Y.js の CRDT 機能、WebSocket による同期機能が利用可能になります。
型定義
TypeScript で型安全な実装を行うため、まず型定義を行います。
typescript// types.ts
import * as Y from 'yjs';
/**
* 共有ドキュメントの状態を表す型
* CRDT で管理される実際のデータ構造
*/
export interface SharedDocument {
title: string;
content: string;
updatedAt: number;
}
/**
* Zustand Store の状態型
* React コンポーネントが参照する状態
*/
export interface DocumentStore {
// 現在のドキュメント状態
document: SharedDocument;
// Y.js のドキュメントインスタンス(内部使用)
ydoc: Y.Doc | null;
// 接続状態
isConnected: boolean;
}
この型定義により、アプリケーション全体で一貫した型安全性が保たれます。
Y.js ドキュメントの初期化
次に、Y.js のドキュメントを初期化する関数を作成します。
typescript// yjs-setup.ts
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
/**
* Y.js ドキュメントとプロバイダーを初期化
* @param roomId - 共有ルームの識別子
* @returns Y.Doc インスタンスとプロバイダー
*/
export function initializeYDoc(roomId: string) {
// Y.js のドキュメントを作成
const ydoc = new Y.Doc();
// WebSocket プロバイダーを設定
// 実際の環境では、WebSocket サーバーの URL を指定
const provider = new WebsocketProvider(
'ws://localhost:1234',
roomId,
ydoc
);
return { ydoc, provider };
}
WebsocketProvider は、Y.js のドキュメントを WebSocket 経由で他のクライアントと同期するための機能を提供します。
Zustand Store の作成
ここが最も重要な部分です。Y.js と Zustand を統合した Store を作成します。
typescript// store.ts
import { create } from 'zustand';
import * as Y from 'yjs';
import { DocumentStore, SharedDocument } from './types';
import { initializeYDoc } from './yjs-setup';
/**
* Y.js の Map を SharedDocument に変換
*/
function yMapToDocument(ymap: Y.Map<any>): SharedDocument {
return {
title: ymap.get('title') || '',
content: ymap.get('content') || '',
updatedAt: ymap.get('updatedAt') || Date.now(),
};
}
この変換関数により、Y.js の内部表現を、アプリケーションで使いやすい形式に変換します。
typescript/**
* CRDT 対応のドキュメント Store を作成
*/
export const useDocumentStore = create<DocumentStore>(
(set, get) => {
let ydoc: Y.Doc | null = null;
let ymap: Y.Map<any> | null = null;
let updateFromYjs = false; // 無限ループ防止フラグ
return {
document: {
title: '',
content: '',
updatedAt: Date.now(),
},
ydoc: null,
isConnected: false,
};
}
);
初期状態を定義しています。updateFromYjs フラグは、Y.js からの更新と Zustand からの更新を区別するために使用されます。
初期化関数の実装
Store に CRDT ドキュメントを接続する初期化関数を追加します。
typescript/**
* Y.js ドキュメントを初期化し、Zustand と接続
*/
export function initializeDocument(roomId: string) {
const store = useDocumentStore.getState();
// Y.js のセットアップ
const { ydoc, provider } = initializeYDoc(roomId);
const ymap = ydoc.getMap('document');
// Y.js の変更を Zustand に反映
ymap.observe((event) => {
updateFromYjs = true;
useDocumentStore.setState({
document: yMapToDocument(ymap),
});
updateFromYjs = false;
});
// 初期状態を設定
useDocumentStore.setState({
ydoc,
document: yMapToDocument(ymap),
});
}
ymap.observe() により、Y.js ドキュメントの変更を監視し、Zustand の状態に反映しています。これで他のユーザーの変更が自動的に UI に反映されますね。
更新アクションの実装
ドキュメントを更新するアクション関数を作成します。
typescript/**
* ドキュメントのタイトルを更新
*/
export function updateTitle(newTitle: string) {
const { ydoc } = useDocumentStore.getState();
if (!ydoc) return;
const ymap = ydoc.getMap('document');
// トランザクション内で更新を実行
ydoc.transact(() => {
ymap.set('title', newTitle);
ymap.set('updatedAt', Date.now());
});
// 楽観的 UI 更新(Y.js の observe で再度更新されるが、ちらつき防止)
if (!updateFromYjs) {
useDocumentStore.setState({
document: yMapToDocument(ymap),
});
}
}
ydoc.transact() を使用することで、複数の変更を一つのトランザクションとして扱い、効率的に同期できます。
typescript/**
* ドキュメントの内容を更新
*/
export function updateContent(newContent: string) {
const { ydoc } = useDocumentStore.getState();
if (!ydoc) return;
const ymap = ydoc.getMap('document');
ydoc.transact(() => {
ymap.set('content', newContent);
ymap.set('updatedAt', Date.now());
});
if (!updateFromYjs) {
useDocumentStore.setState({
document: yMapToDocument(ymap),
});
}
}
同様に、コンテンツ更新用の関数も実装します。これで、ユーザーの入力が即座に CRDT ドキュメントに反映されます。
React コンポーネントでの使用
作成した Store を React コンポーネントで使用する例です。
typescript// DocumentEditor.tsx
import React, { useEffect } from 'react';
import {
useDocumentStore,
initializeDocument,
updateTitle,
updateContent,
} from './store';
export function DocumentEditor({
roomId,
}: {
roomId: string;
}) {
const { document, isConnected } = useDocumentStore();
// コンポーネントマウント時に初期化
useEffect(() => {
initializeDocument(roomId);
}, [roomId]);
return (
<div>
{/* 接続状態の表示 */}
<div>{isConnected ? '✓ 接続済み' : '接続中...'}</div>
</div>
);
}
コンポーネントのマウント時に CRDT ドキュメントを初期化し、接続状態を表示しています。
typescript {/* タイトル入力 */}
<input
type="text"
value={document.title}
onChange={(e) => updateTitle(e.target.value)}
placeholder="ドキュメントタイトル"
/>
{/* コンテンツ入力 */}
<textarea
value={document.content}
onChange={(e) => updateContent(e.target.value)}
placeholder="コンテンツを入力..."
rows={10}
/>
{/* 最終更新時刻 */}
<div>
最終更新: {new Date(document.updatedAt).toLocaleString()}
</div>
</div>
);
}
入力フィールドは Zustand の状態にバインドされ、変更は自動的に CRDT ドキュメントに同期されます。非常にシンプルな API で、複雑なリアルタイム同期が実現できていますね。
Automerge と Zustand の連携実装
次に、Automerge を使った実装例を見ていきましょう。Automerge は JSON ライクなデータ構造を扱えるため、複雑な状態管理に適しています。
インストール
bashyarn add @automerge/automerge zustand
yarn add -D @types/automerge
Automerge の最新版を使用します。
型定義
typescript// automerge-types.ts
import * as Automerge from '@automerge/automerge';
/**
* Automerge で管理するドキュメント型
*/
export interface AutomergeDocument {
title: string;
content: string;
metadata: {
createdAt: number;
updatedAt: number;
author: string;
};
collaborators: string[];
}
/**
* Zustand Store の状態型
*/
export interface AutomergeStore {
// Automerge ドキュメント
doc: Automerge.Doc<AutomergeDocument> | null;
// ローカル状態(読み取り専用)
state: AutomergeDocument | null;
// 同期状態
syncState: 'disconnected' | 'connecting' | 'connected';
}
Automerge の Doc 型を使用して、型安全なドキュメントを定義します。
Automerge ドキュメントの初期化
typescript// automerge-setup.ts
import * as Automerge from '@automerge/automerge';
import { AutomergeDocument } from './automerge-types';
/**
* 新しい Automerge ドキュメントを作成
*/
export function createDocument(
author: string
): Automerge.Doc<AutomergeDocument> {
return Automerge.from({
title: '',
content: '',
metadata: {
createdAt: Date.now(),
updatedAt: Date.now(),
author,
},
collaborators: [author],
});
}
Automerge.from() を使用して、初期状態からドキュメントを作成します。
Zustand Store の作成
typescript// automerge-store.ts
import { create } from 'zustand';
import * as Automerge from '@automerge/automerge';
import {
AutomergeStore,
AutomergeDocument,
} from './automerge-types';
import { createDocument } from './automerge-setup';
export const useAutomergeStore = create<AutomergeStore>(
(set, get) => ({
doc: null,
state: null,
syncState: 'disconnected',
})
);
初期状態を定義します。
初期化とアクション
typescript/**
* Automerge ドキュメントを初期化
*/
export function initAutomerge(author: string) {
const doc = createDocument(author);
useAutomergeStore.setState({
doc,
state: doc as AutomergeDocument,
syncState: 'connected',
});
}
/**
* タイトルを更新
*/
export function updateAutomergeTitle(newTitle: string) {
const { doc } = useAutomergeStore.getState();
if (!doc) return;
// Automerge の change API を使用
const newDoc = Automerge.change(doc, (draft) => {
draft.title = newTitle;
draft.metadata.updatedAt = Date.now();
});
useAutomergeStore.setState({
doc: newDoc,
state: newDoc as AutomergeDocument,
});
}
Automerge.change() は、Immer のような API で、イミュータブルな更新を実現します。
typescript/**
* 共同作業者を追加
*/
export function addCollaborator(name: string) {
const { doc } = useAutomergeStore.getState();
if (!doc) return;
const newDoc = Automerge.change(doc, (draft) => {
if (!draft.collaborators.includes(name)) {
draft.collaborators.push(name);
}
});
useAutomergeStore.setState({
doc: newDoc,
state: newDoc as AutomergeDocument,
});
}
配列への追加も、通常の JavaScript と同じように記述できます。
ドキュメントの同期
Automerge では、変更をバイナリ形式で効率的に送受信できます。
typescript/**
* 変更をエクスポート(他のクライアントに送信)
*/
export function exportChanges(): Uint8Array | null {
const { doc } = useAutomergeStore.getState();
if (!doc) return null;
// ドキュメント全体をバイナリで保存
return Automerge.save(doc);
}
/**
* 変更をインポート(他のクライアントから受信)
*/
export function importChanges(binary: Uint8Array) {
try {
const doc = Automerge.load<AutomergeDocument>(binary);
useAutomergeStore.setState({
doc,
state: doc as AutomergeDocument,
});
} catch (error) {
console.error('Failed to import changes:', error);
}
}
Automerge.save() と Automerge.load() により、ドキュメントのシリアライズ・デシリアライズが可能です。
マージ処理
複数のクライアントからの変更をマージする処理を実装します。
typescript/**
* 他のドキュメントとマージ
*/
export function mergeDocuments(remoteBinary: Uint8Array) {
const { doc } = useAutomergeStore.getState();
if (!doc) return;
try {
// リモートドキュメントを読み込み
const remoteDoc =
Automerge.load<AutomergeDocument>(remoteBinary);
// 自動的にマージ(競合は CRDT により解決)
const mergedDoc = Automerge.merge(doc, remoteDoc);
useAutomergeStore.setState({
doc: mergedDoc,
state: mergedDoc as AutomergeDocument,
});
} catch (error) {
console.error('Merge failed:', error);
}
}
Automerge.merge() により、競合を自動的に解決しながらマージが行われます。これが CRDT の強力な機能ですね。
エラーハンドリングとデバッグ
実際の運用では、適切なエラーハンドリングが重要です。
接続エラーの処理
typescript// error-handling.ts
/**
* WebSocket 接続エラーのハンドリング
*/
export function handleConnectionError(error: Error) {
// エラーコード: WebSocket Connection Failed
console.error(
'WebSocket Connection Failed:',
error.message
);
// Zustand の状態を更新
useDocumentStore.setState({
isConnected: false,
});
// ユーザーへの通知
showNotification({
type: 'error',
message:
'サーバーとの接続に失敗しました。再接続を試みています...',
});
// 再接続の試行
setTimeout(() => {
reconnect();
}, 3000);
}
エラーコード: WebSocket Connection Failed
発生条件: WebSocket サーバーへの接続が失敗した場合
解決方法:
- ネットワーク接続を確認
- WebSocket サーバーの稼働状態を確認
- 自動再接続を実装
同期エラーの処理
typescript/**
* 同期エラーのハンドリング
*/
export function handleSyncError(error: Error) {
// エラーコード: CRDT Sync Error
console.error('CRDT Sync Error:', error.message);
// エラーの種類に応じた処理
if (error.message.includes('version mismatch')) {
// バージョン不一致の場合、フル同期を実行
performFullSync();
} else if (error.message.includes('invalid update')) {
// 無効な更新の場合、ローカルの変更を破棄
resetToRemoteState();
}
}
エラーコード: CRDT Sync Error - version mismatch
発生条件: クライアント間でドキュメントのバージョンが一致しない場合
解決方法:
- フル同期を実行して状態を再構築
- ローカルキャッシュをクリア
- 必要に応じて手動でのマージを促す
デバッグ用ロギング
開発時には、詳細なログが役立ちます。
typescript/**
* CRDT 操作のデバッグログ
*/
export function logCRDTOperation(
operation: string,
details: any
) {
if (process.env.NODE_ENV === 'development') {
console.group(`[CRDT] ${operation}`);
console.log('Timestamp:', new Date().toISOString());
console.log('Details:', details);
console.log(
'Current State:',
useDocumentStore.getState().document
);
console.groupEnd();
}
}
この関数を各操作の前後で呼び出すことで、CRDT の動作を追跡できます。
パフォーマンス最適化
メモ化による再レンダリングの抑制
typescript// optimizations.ts
import { useDocumentStore } from './store';
import { useMemo } from 'react';
/**
* 選択的な状態購読(必要な部分のみ)
*/
export function useDocumentTitle() {
return useDocumentStore((state) => state.document.title);
}
export function useDocumentContent() {
return useDocumentStore(
(state) => state.document.content
);
}
Zustand の selector 機能により、必要な状態の変更時のみ再レンダリングが発生します。
バッチ更新
typescript/**
* 複数の変更をバッチで適用
*/
export function batchUpdateDocument(
updates: Partial<SharedDocument>
) {
const { ydoc } = useDocumentStore.getState();
if (!ydoc) return;
const ymap = ydoc.getMap('document');
// トランザクションで一括更新
ydoc.transact(() => {
Object.entries(updates).forEach(([key, value]) => {
ymap.set(key, value);
});
ymap.set('updatedAt', Date.now());
});
}
トランザクションを使用することで、ネットワークトラフィックを削減できますね。
まとめ
本記事では、CRDT(Y.js と Automerge)と Zustand を組み合わせたリアルタイム共同編集の実装方法を解説しました。
重要なポイント:
-
役割分担の明確化: Zustand は React との連携を、CRDT は競合解決と同期を担当する設計により、シンプルで保守性の高いコードが実現できます
-
型安全性の確保: TypeScript の型定義により、CRDT ドキュメントと Zustand の状態の整合性を保ち、実行時エラーを防げます
-
選択肢の理解: Y.js はテキスト編集に特化し高パフォーマンス、Automerge は JSON 構造の扱いやすさが特徴です。用途に応じて選択しましょう
-
エラーハンドリング: 接続エラーや同期エラーへの適切な対応により、ユーザー体験を損なわないシステムが構築できます
-
パフォーマンス最適化: selector によるメモ化や、トランザクションによるバッチ更新で、スケーラブルなアプリケーションを実現できます
この設計パターンを活用すれば、Google Docs のようなリアルタイム共同編集機能を、比較的少ないコード量で実装できるでしょう。CRDT という強力な技術と、Zustand のシンプルさを組み合わせることで、開発体験とユーザー体験の両方を向上させることが可能です。
リアルタイム共同編集は、現代の Web アプリケーションにおいて欠かせない機能となっています。本記事で紹介した手法が、皆さんのプロジェクトに役立てば幸いです。
関連リンク
articleCRDT × Zustand:Y.js/Automerge 連携でリアルタイム共同編集を設計
articlezustand Middleware 合成チートシート:logger・persist・immer・subscribeWithSelector の重ね方
articleStorybook で Zustand をモックする:Controls 連動とシナリオ駆動 UI
article伝搬方式を比較:Zustand の selector/derived-middleware/外部 reselect の使い分け
articleZustand subscribeWithSelector で発生する古い参照問題:メモ化と equalityFn の落とし穴
articleZustand × useTransition 概説:並列レンダリング時代に安全な更新を設計する
articleCRDT × Zustand:Y.js/Automerge 連携でリアルタイム共同編集を設計
articleSvelte URL ドリブン設計:検索パラメータとストアの同期パターン
articleKubernetes で WebSocket:Ingress(NGINX/ALB) 設定とスティッキーセッションの実装手順
articleStorybook × Design Tokens 設計:Style Dictionary とテーマ切替の連携
articleWebRTC Simulcast 設計ベストプラクティス:レイヤ数・ターゲットビットレート・切替条件
articleSolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来