はじめに
オフラインでもサクサク動いて、ネットが繋がった瞬間に自動でサーバーと同期される。そんなアプリを作りたいと思ったことはないでしょうか。
実際に作ろうとすると、これがなかなか大変です。ローカルのデータストアを用意して、サーバーとの差分を検知して、コンフリクトが起きたら解決して……と考えることが山積みになります。素直にfetchでAPIを叩くだけの設計では、電波の悪い場所や機内モードになった瞬間にアプリが固まってしまいます。
RxDBは、この「ローカルファースト」なアプリ設計を丸ごと引き受けてくれるNoSQLデータベースです。GitHubで23,000以上のスターを獲得しており、直近の更新も前日という活発さ。ライセンスはApache License 2.0なので商用利用も安心して検討できます。
RxDBとは
RxDBは、ブラウザやNode.js、React Nativeなど、あらゆるJavaScriptランタイム上で直接動作するNoSQLデータベースです。アプリ内にデータベースそのものを持つことで、ネットワークが切れていてもゼロレイテンシーでクエリを実行できます。
名前の「Rx」が示す通り、RxJSのObservableをベースにした「Reactive」なクエリが最大の特徴。データが変化すると、それを購読しているUIが自動的に更新されます。
主な特徴
- ローカルファースト設計 - データはまずローカルに保存され、オフラインでも即座に読み書きできる
- Observable Realtime Queries - クエリ結果をSubscribeするだけで、データ変更がリアルタイムにUIへ反映される
- 柔軟なバックエンド連携 - CouchDB、GraphQL、Firebase、Supabase、MongoDB、独自のHTTPサーバーなど、既存のバックエンドと同期できる
- JSON Schemaによるスキーマ定義 - 型やインデックスを宣言的に記述でき、TypeScriptとの相性も良い
- マルチプラットフォーム対応 - React、Vue、Angular、Svelte、Electron、Ionic、React Native、Capacitorなど幅広いフレームワークで利用可能
インストール
npmまたはyarnでインストールします。
npm install rxdb rxjs
yarn add rxdb rxjs
RxDBはRxJSに依存しているため、あわせてインストールしておきましょう。
基本的な使い方
まずはデータベースを作成し、コレクション(テーブルに近い概念)を追加してみます。ストレージにはブラウザで手軽に使えるstorage-dexie(IndexedDBベース)を利用します。
import { createRxDatabase, addRxPlugin } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
// データベースの作成
const db = await createRxDatabase({
name: 'todo_database',
storage: getRxStorageDexie(),
});
// スキーマの定義(JSON Schema形式)
const todoSchema = {
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string', maxLength: 100 },
title: { type: 'string' },
done: { type: 'boolean' },
createdAt: { type: 'string', format: 'date-time' },
},
required: ['id', 'title', 'done'],
};
// コレクションの追加
await db.addCollections({
todos: { schema: todoSchema },
});
データの追加とリアルタイムクエリはこのように書けます。
// ドキュメントの追加
await db.todos.insert({
id: 'todo-1',
title: 'RxDBの記事を書く',
done: false,
createdAt: new Date().toISOString(),
});
// Observableなクエリ(データ変更時に自動で流れてくる)
const subscription = db.todos
.find({ selector: { done: false } })
.$.subscribe((todos) => {
console.log('未完了のタスク数:', todos.length);
});
// 購読解除
subscription.unsubscribe();
insertやupdateでデータを変更すると、購読しているfind().$が自動で新しい結果を流してくれます。ReactやVueのstateと組み合わせれば、DOM更新のためのコードを自前で書く必要がなくなります。
実践的なユースケース
Reactコンポーネントとの連携
React Hooksと組み合わせると、リアルタイムなUIを簡潔に実装できます。
import { useEffect, useState } from 'react';
import type { RxDatabase } from 'rxdb';
function TodoList({ db }: { db: RxDatabase }) {
const [todos, setTodos] = useState<any[]>([]);
useEffect(() => {
const subscription = db.todos
.find({ selector: { done: false } })
.$.subscribe((docs) => setTodos(docs));
return () => subscription.unsubscribe();
}, [db]);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
コンポーネントのアンマウント時にunsubscribe()を呼ぶのを忘れないようにしましょう。メモリリークの原因になります。
バックエンドとのレプリケーション
RxDBの真骨頂は、既存のバックエンドとの同期機能です。以下は独自のHTTPサーバーと同期する例です。
import { replicateRxCollection } from 'rxdb/plugins/replication';
const replicationState = replicateRxCollection({
collection: db.todos,
replicationIdentifier: 'my-todo-replication',
live: true,
pull: {
handler: async (checkpoint) => {
const response = await fetch('/api/todos/pull', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
return {
documents: data.documents,
checkpoint: data.checkpoint,
};
},
},
push: {
handler: async (changeRows) => {
const response = await fetch('/api/todos/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changeRows }),
});
return response.json();
},
},
});
pullでサーバーから差分を取得し、pushでローカルの変更をサーバーへ送信します。live: trueにしておけば、オンライン中は継続的に同期が走り続けます。オフラインになっても書き込みはローカルに蓄積され、再接続時に自動で同期されるのが大きな利点です。
まとめ
RxDBを使うと、オフライン対応・リアルタイム同期といった、本来なら実装コストの高い機能を、宣言的なスキーマとObservableなクエリだけで実現できます。今回紹介したのは基本的な使い方の一部で、ほかにもコンフリクト解決の仕組みや、CouchDB・GraphQL・Firebaseなど各種バックエンドへのレプリケーションプラグインが用意されています。
PWAやモバイルアプリなど「ネットワークが不安定な環境でも快適に使えるアプリ」を作りたい方は、まず公式のQuickstartから試してみることをおすすめします。