tanstack-db について
tanstack-dbは、差分データフローを基盤とするベータ版のクライアントサイド組み込みデータベース層、TanStack DBを解説するスキルです。正規化コレクション、リアクティブなライブクエリ、楽観的ミューテーション、TanStack Queryや複数のデータソースとの連携を扱います。
主な機能
- 01複数のデータソースに対応する正規化コレクション
- 02SQL風ビルダーによるリアクティブなライブクエリ
- 03楽観的更新と失敗時の自動ロールバック
- 04インクリメンタルなクエリ計算
- 05イーガーおよびオンデマンド同期モード
活用シーン
- 01クライアントアプリのリアクティブなデータビュー構築
- 02ミューテーションの同期中にUIを即時更新
- 03正規化コレクションのクエリと結合
- 04大規模コレクションのオンデマンド読み込み
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ tanstack-skills/ tanstack-skills --skill tanstack-dbインストール後、Claude Code で/tanstack-dbを入力して読み込まれたか確認します。
詳細
概要
TanStack DB は、差分データフローを基盤とするクライアントサイドの組み込みデータベースレイヤーです。正規化されたコレクションを維持し、ライブクエリに増分計算を使用し、自動的な楽観的ミューテーションを提供するとともに、データフェッチでは TanStack Query と連携します。10 万行を超えるデータでも、更新は 1 ミリ秒未満です。
パッケージ: @tanstack/react-db
クエリ連携: @tanstack/query-db-collection
ステータス: ベータ版(v0.5)
インストール
npm install @tanstack/react-db @tanstack/query-db-collection
主な概念
- コレクション:データソース(TanStack Query、Electric など)をラップする正規化されたデータストア
- ライブクエリ:SQL に似たクエリビルダーを使用するリアクティブなサブスクリプション
- 楽観的ミューテーション:失敗時のロールバックを伴う、自動的で即時の UI 更新
- 差分データフロー:変更の影響を受けるクエリ結果のみを再計算します
コレクション
コレクションの作成
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => api.todos.getAll(),
getKey: (item) => item.id,
schema: todoSchema,
onInsert: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((mutation) =>
api.todos.create(mutation.modified)
)
)
},
onUpdate: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((mutation) =>
api.todos.update(mutation.modified)
)
)
},
onDelete: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map((mutation) =>
api.todos.delete(mutation.original.id)
)
)
},
})
)
同期モード
// Eager (default): Load entire collection upfront. Best for <10k rows.
const smallCollection = createCollection(
queryCollectionOptions({ syncMode: 'eager', /* ... */ })
)
// On-Demand: Load only what queries request. Best for >50k rows, search.
const largeCollection = createCollection(
queryCollectionOptions({
syncMode: 'on-demand',
queryFn: async (ctx) => {
const params = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
return api.getProducts(params)
},
})
)
// Progressive: Load query subset immediately, full sync in background.
const collaborativeCollection = createCollection(
queryCollectionOptions({ syncMode: 'progressive', /* ... */ })
)
ライブクエリ
基本的なクエリ
import { useLiveQuery } from '@tanstack/react-db'
import { eq } from '@tanstack/db'
function TodoList() {
const { data: todos } = useLiveQuery((query) =>
query
.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.completed, false))
)
return <ul>{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>
}
クエリビルダー API
const { data } = useLiveQuery((q) =>
q
.from({ t: todoCollection })
.where(({ t }) => eq(t.status, 'active'))
.orderBy(({ t }) => t.createdAt, 'desc')
.limit(10)
)
結合
const { data } = useLiveQuery((q) =>
q
.from({ t: todoCollection })
.innerJoin(
{ u: userCollection },
({ t, u }) => eq(t.userId, u.id)
)
.innerJoin(
{ p: projectCollection },
({ u, p }) => eq(u.projectId, p.id)
)
.where(({ p }) => eq(p.id, currentProject.id))
)
フィルター演算子
import { eq, lt, and } from '@tanstack/db'
// Equality
eq(field, value)
// Less than
lt(field, value)
// AND
and(eq(product.category, 'electronics'), lt(product.price, 100))
並べ替えと件数制限
const { data } = useLiveQuery((q) =>
q
.from({ product: productsCollection })
.where(({ product }) =>
and(eq(product.category, 'electronics'), lt(product.price, 100))
)
.orderBy(({ product }) => product.price, 'asc')
.limit(10)
)
楽観的ミューテーション
挿入
todoCollection.insert({
id: uuid(),
text: 'New todo',
completed: false,
})
// Immediately: updates all live queries referencing this collection
// Background: calls onInsert handler to sync with server
// On failure: automatic rollback
手動の定型コードは不要
| 以前(TanStack Query のみ) | 以後(TanStack DB) |
|---|---|
楽観的な状態のための手動 onMutate | 自動 |
手動の onError ロールバックロジック | 自動 |
| ミューテーションごとのキャッシュ無効化 | すべてのライブクエリが自動的に更新 |
クエリ駆動の同期(オンデマンド)
ライブクエリは、最適化されたネットワークリクエストを自動的に生成します。
// This live query...
useLiveQuery((q) =>
q.from({ product: productsCollection })
.where(({ product }) => and(eq(product.category, 'electronics'), lt(product.price, 100)))
.orderBy(({ product }) => product.price, 'asc')
.limit(10)
)
// ...automatically generates:
// GET /api/products?category=electronics&price_lt=100&sort=price:asc&limit=10
述語のマッピング
queryFn: async (ctx) => {
const { filters, sorts, limit } = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
const params = new URLSearchParams()
filters.forEach(({ field, operator, value }) => {
if (operator === 'eq') params.set(field.join('.'), String(value))
else if (operator === 'lt') params.set(`${field.join('.')}_lt`, String(value))
})
if (limit) params.set('limit', String(limit))
return fetch(`/api/products?${params}`).then(r => r.json())
}
パフォーマンス
| 操作 | レイテンシー |
|---|---|
| 単一行の更新(10 万件のソート済みコレクション) | 約 0.7 ms |
| 後続のクエリ(同期後) | 1 ms 未満 |
| コレクション間の結合 | 1 ミリ秒未満 |
サポートされているコレクションの種類
- Query Collection - TanStack Query 連携
- Electric Collection - Electric SQL のリアルタイム同期
- TrailBase Collection - TrailBase バックエンド
- RxDB Collection - RxDB 連携
- PowerSync Collection - PowerSync 同期
- LocalStorage Collection - ブラウザーへの永続化
- LocalOnly Collection - メモリー内のみ
API の概要
import { createCollection, useLiveQuery } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
import { eq, lt, and, parseLoadSubsetOptions } from '@tanstack/db'
ベストプラクティス
- コレクションはモジュールレベルで定義する - シングルトンです
- 適切な同期モードを選ぶ:
eager(1 万件未満)、on-demand(5 万件超)、progressive(共同作業向け) - ビュー固有の API ではなく結合を使用する - 正規化されたコレクションを一度だけ読み込みます
- フェッチは TanStack Query に任せる - DB は Query を拡張するものであり、置き換えるものではありません
parseLoadSubsetOptionsを使用する - ライブクエリの述語を API パラメーターにマッピングします- 自動の楽観的更新を活用する - 楽観的な状態を手動で管理しないでください
- 実行時の検証と TypeScript の型推論にはスキーマを使用する
- 増分計算を活用する - 手動の
.filter()ではなく、フィルタリングをエンジンに任せます
よくある落とし穴
- コンポーネント内でコレクションを作成する(モジュールレベルで作成する必要があります)
- TanStack Query を完全に置き換えようとする(DB はその上に構築されています)
- ライブクエリの
where句の代わりに、レンダー内で手動の.filter()を使用する - 適切な正規化のための
getKeyを指定しない - サーバー同期用のミューテーションハンドラー(
onInsert、onUpdate、onDelete)を指定し忘れる
コメント
Markdown 対応