TanStack DBは、名前にDBと付きますが公式リポジトリの説明は「The reactive client store for your API.」です。ブラウザにSQLiteを持ち込む類のものではなく、正規化したデータをメモリ上のコレクションに載せ、そこへSQLに似たライブクエリを投げる層を追加します。2026年9月21日時点の@tanstack/dbは0.9.2で、READMEはBETAと明記しています。ここではTanStack Queryとの役割分担、v0.9系で実際に動く書き方、そして誤解されやすい永続化の範囲を、公式docsの記述と手元での実行結果に沿って整理します。
まとめ
- TanStack QueryをTanStack DBに置き換えるのではない。公式Overviewは拡張だと明記しており、取得はQuery、保持と検索はDBが担う。
- 2026年9月21日時点で
@tanstack/db0.9.2、@tanstack/react-db0.4.1。1.0には未到達で、0.9.0でも公開APIの削除がある。 whereにJavaScriptの比較演算子は書けない。eqやnotなどの式関数で組む。旧来の書き方は型エラーにならず実行時に落ちる。- 同梱のローカルコレクションは
localStorageCollectionOptionsとlocalOnlyCollectionOptionsの2つ。強い永続化はpersistedCollectionOptionsのSQLite層、オフライン書き込みは@tanstack/offline-transactionsが別パッケージで担う。 - 公称の0.7msはM1 Pro MacBookで10万件のソート済みコレクションの1行を更新したときの値であり、環境非依存の保証値ではない。
TanStack DBの正体と現在地
データベースではなく、APIの前に置くリアクティブなストア
GitHubのTanStack/dbはリポジトリ説明を「The reactive client store for your API.」としています。ストレージエンジンではなく、APIやsyncエンジンから取り込んだ行を正規化コレクションとして保持し、コンポーネントのクエリ結果を差分で更新し続ける層です。
差分更新はd2tsというdifferential dataflowのTypeScript実装が支えています。公式Overviewは「Updating one row in a sorted 100,000-item collection completes in ~0.7ms on an M1 Pro MacBook」と数値を出しますが、特定マシンでのベンチマーク値なので自社環境の見積もりには使えません。0.1のリリース記事は2025年7月30日付、Kyle Mathews氏とSam Willis氏の署名です。
0.9系の版構成と削除された公開API
READMEの冒頭には「Tanstack DB is currently in BETA.」とあり、版も1.0手前です。
| パッケージ | 版(2026-09-21時点) | 役割 |
|---|---|---|
| @tanstack/db | 0.9.2 | コレクション・ライブクエリの本体 |
| @tanstack/react-db | 0.4.1 | Reactアダプタ(coreを再エクスポート) |
| @tanstack/db-ivm | 0.1.22 | 差分更新エンジン |
| @tanstack/query-db-collection | 1.2.15 | TanStack Query連携 |
| @tanstack/electric-db-collection | 0.4.10 | ElectricSQL連携 |
ライセンスはMITです。週間ダウンロード数は2026年9月13日から19日の集計で@tanstack/dbが651,782、@tanstack/react-queryが48,509,300、比にすると74.4倍の開きです。依存解決やCIでの取得も含む値なので利用者数そのものではありませんが、採用の厚みには差があります。0.9.0のCHANGELOGは「Remove getStats() and IndexStats; use index.keyCount for the current entry count」「Remove the live-query utils.getRunCount() diagnostic」と記載しており、診断系とはいえ公開APIが消えています。版を固定せずに運用すると、マイナー更新で計測コードが壊れます。
TanStack QueryとTanStack DBの違いと使い分け
TanStack Queryを置き換えず拡張する設計
「TanStack Queryの後継」と受け取られることがありますが、公式Overviewは「It extends TanStack Query with collections, live queries and optimistic mutations, working seamlessly with REST APIs, sync engines, or any data source」で、拡張だと明言しています。
実際の接続点がqueryCollectionOptionsです。queryKey・queryFn・queryClientをそのまま渡すため、既存のTanStack Queryとは?React Queryとの違い・v5の使い方と脆弱性対策で組んだ取得処理を捨てる必要はありません。ネットワークからの取得はQueryが担当し、取り込んだ行の保持と検索をDBが引き受けます。
Queryの作法がそのまま通らない箇所
ただし、Queryのオプションが全部そのまま使えるわけではありません。placeholderDataは公式docsが「placeholderData is intentionally unsupported.」と書いており、意図的に非対応です。プレースホルダは観測側のUI状態であり、コレクション全体の正規化済み行として実体化すべきではない、というのが理由です。表示用のダミーはUI側で描画します。
queryKey・queryFn・queryClient・select・meta・subscribed・structuralSharing・notifyOnChangePropsはコレクション側が所有するため、Query observerのオプションとしては露出しません。特にselectは「レスポンスから行配列を取り出す」用途に再定義されており、TanStack Queryのselectとは契約が違います。読み替えを怠ると変換処理が二重にかかります。
どちらで足りるかの判断基準
画面ごとに専用エンドポイントを足し続けている、1回取得したデータを絞り込みや結合で何通りにも見せたい、楽観的更新のキャッシュ書き換えがバグの温床になっている。この3つのどれかに当てはまるならDBを重ねる価値があります。Zustandとは?Reactの状態管理を最小コードで実現する使い方を徹底解説【v5対応】のようなUI状態専用のストアと競合するものでもありません。
導入とコレクション定義(v0.9系の書き方)
インストールと役割分担
Reactの最小構成は次の3つです。フレームワークパッケージはコアの@tanstack/dbを全て再エクスポートするため、コアを個別に入れる必要はありません。対応するReactはv16.8以降です。
npm install @tanstack/react-db @tanstack/query-db-collection @tanstack/query-core
ElectricSQLなら@tanstack/electric-db-collection、TrailBaseなら@tanstack/trailbase-db-collection、PowerSyncやRxDBならそれぞれのパッケージを足します。供給元を差し替えてもライブクエリの書き方は変わりません。
DbClientとcollectionOptionsの最小構成
v0.9系の公式Quick Startは、コレクション定義をcollectionOptionsという記述子に分け、DbClient経由で実体化します。createCollectionの直呼びも残っていますが、SSRを含む構成では記述子とクライアントを分けるこちらが前提です。
import { DbClient, DbProvider, collectionOptions, eq, useDbClient, useLiveQuery } from '@tanstack/react-db'
import { QueryClient } from '@tanstack/query-core'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
type Todo = { id: string; text: string; completed: boolean }
const dbClient = new DbClient({ queryClient: new QueryClient() })
const todoCollection = collectionOptions('todos', (client) =>
queryCollectionOptions({
id: 'todos',
queryKey: ['todos'],
queryClient: client.requireDependency<QueryClient>('queryClient'),
queryFn: async (): Promise<Array<Todo>> => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error(`GET /api/todos failed: ${res.status}`)
return res.json()
},
getKey: (item) => item.id,
onUpdate: async ({ transaction }) => {
const { original, modified } = transaction.mutations[0]
const res = await fetch(`/api/todos/${original.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(modified),
})
if (!res.ok) throw new Error(`PUT failed: ${res.status}`)
},
})
)
function Todos() {
const todos = useDbClient().collection(todoCollection)
const { data } = useLiveQuery({
query: (q) => q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.completed, false)),
})
return (
<ul>
{data.map((t) => (
<li key={t.id} onClick={() => todos.update(t.id, (d) => { d.completed = true })}>{t.text}</li>
))}
</ul>
)
}
export const App = () => (
<DbProvider client={dbClient}><Todos /></DbProvider>
)
このコードはTypeScript 5.9.2・@tanstack/react-db 0.4.1・@tanstack/query-db-collection 1.2.15で型チェックが通ることを確認しています。押さえどころは3点です。queryFnに戻り値型を付けないと行の型がobjectに落ち、todo.completedの参照でTS2339になります。fetchは4xxや5xxで例外を投げないので、res.okを見て自分でthrowしないとロールバックが働きません。useDbClientはDbProviderの配下でしか使えません。
getKeyも必須です。updateとdeleteは対象行をキーで特定するため、省くとミューテーションが成立しません。スキーマはStandard Schema互換であればよく、ZodやValibotをそのまま渡せます。なおインストールコマンドは実行時点の最新版を取りに行くので、挙動を再現するなら版を固定してロックファイルを管理してください。
syncModeによる読み込み範囲の指定
公式Overviewは3つの読み込み方式を挙げます。既定のeagerは全体を先読みする方式で、docsは1万行未満の静的データ向けとしています。on-demandはクエリが要求した分だけ読み込むモードで、5万行超やカタログ・検索画面が対象です。progressiveは先に部分を返し、残りを背後で同期します。ただし0.9.2のコアのSyncMode型はeagerとon-demandの2値で、progressiveはElectric Collection専用です。Query Collectionには指定できません。
on-demandでは、コンポーネントのクエリ条件がctx.meta.loadSubsetOptionsとしてqueryFnに渡ります。自動でAPIへ適用されるわけではなく、parseLoadSubsetOptionsなどで条件を取り出し、自社のAPI仕様に合うパラメータへ変換して送るのはqueryFn側の実装です。docsは「Do not create a collection for each where, orderBy, or limit」と明記しており、条件ごとにコレクションを増やすのは誤用です。コレクションはサーバー側のリソース単位で1つ作ります。
ライブクエリの記法と落とし穴
whereでJavaScript演算子を使ったときの実行時エラー
ここはv0.9系で書き方を間違えやすい箇所です。whereは行ごとに実行される関数ではなく、実行するクエリを宣言する記述です。そのため!todo.completedやtodo.completed === trueのような素のJavaScript式は受け付けません。
やっかいなのは型チェックを通過してしまう点です。検証環境はNode.js 26.5.0・@tanstack/db 0.9.2・TypeScript 5.9.2です。この条件でtscはエラーを出さず、実行時に次の例外で停止しました。
InvalidWhereExpressionError [QueryBuilderError]: Invalid where() expression:
Expected a query expression, but received a boolean.
This usually happens when using JavaScript's comparison operators
(===, !==, <, >, etc.) directly.
正しくは式関数で組みます。@tanstack/react-db 0.4.1のエクスポートを実際に列挙すると、比較・論理・欠損値判定に使うのはeq・gt・gte・lt・lte・like・ilike・inArray・and・or・not・isNull・isUndefinedの13個です。ほかにupperやcoalesce、caseWhenといった値を組み立てる式関数も揃っています。未完了タスクの抽出はwhere(({ todo }) => not(todo.completed))と書きます。whereを複数回つなぐとANDで結合されます。
null比較に適用されるSQL準拠の三値論理
比較の挙動もJavaScriptではなくPostgreSQLに寄せてあります。docsは「Any comparison involving null or undefined evaluates to UNKNOWN, so the row is not matched」としています。
同じ環境でscoreが10・null・nullの3行を入れて確認したところ、eq(todo.score, null)は0件、isNull(todo.score)は2件でした。JavaScriptの感覚でeqにnullを渡すと、エラーも警告も出ないまま結果が静かに空になります。欠損値を拾うときはisNullかisUndefinedを使ってください。NaNもPostgreSQL寄りで、NaN同士は等しく他のあらゆる非null値より大きいものとして並びます。タイムスタンプがNaNの不正なDateも同じ扱いです。
結合とクエリ結果の再利用
ライブクエリはjoinでコレクションをまたげます。正規化して持ち、表示の直前に結合するため、画面専用のAPIを増やさずに済みます。結合はleft・right・inner・fullが揃っています。
クエリ結果自体がまたコレクションになる点も設計に効きます。createLiveQueryCollectionでコンポーネント外に派生コレクションを作り、別のクエリのfromへ渡せます。ただしuseLiveQueryと違い購読の開始と停止は自分で管理します。初期ロード中にサスペンドさせるならuseLiveSuspenseQueryでdataが常に定義済みになります。
楽観的ミューテーションとロールバック
コレクションへの書き込みとハンドラの関係
collection.update(id, draft => ...)を呼ぶと、変更はローカルの楽観的状態として即座に適用され、続いてonUpdateハンドラが呼ばれます。コレクションは同期済みデータと楽観的状態を別に保持し、ライブクエリが読むのは両者を重ねた見え方です。ハンドラが解決すれば確定データに置き換わり、例外を投げればロールバックされます。UI側に巻き戻し処理は要りません。
複数コレクションをまたぐ操作や、サーバー側の計算結果が返る操作ではcreateOptimisticActionを使い、onMutateにローカルの推測値、mutationFnにサーバー呼び出しを書き分けます。さらに細かく制御するならcreateTransactionでtx.mutate・commit・rollbackを明示します。ただし手動トランザクション内の変更ではonInsert・onUpdate・onDeleteは呼ばれず、永続化はトランザクションのmutationFnが担います。
既存の更新処理を残したままのrefetch連携
すでに動いている更新処理があるなら、TanStack DBのミューテーション機構を丸ごと迂回できます。従来どおりAPIを叩いたあと、Query由来のコレクションならcollection.utils.refetch()、ElectricSQLとは?Postgres同期エンジンの仕組み・導入手順と2026年の変化と組む場合はcollection.utils.awaitTxId(txid)で該当トランザクションの同期完了を待つだけで済みます。読み取りと状態管理にだけTanStack DBを使う、という段階的な導入ができます。
永続化とオフライン対応の実装範囲
フレームワークパッケージ同梱の保存先
「クライアントDB」という呼び名から、ブラウザにデータを貯めてオフラインで動くものと期待されがちですが、コレクションの供給元と永続化レイヤーは分かれています。@tanstack/react-db 0.4.1のエクスポートを実際に数えたところ、ローカル向けのコレクションオプションはlocalStorageCollectionOptionsとlocalOnlyCollectionOptionsの2つでした。IndexedDBを直接扱うコレクションはここには含まれません。
localStorage版の位置づけも公式docsが明確に限定しています。「store small amounts of local-only state that persists across browser sessions and syncs across browser tabs in real-time」であり、想定はユーザー設定や表示状態です。コレクション全体を1つのlocalStorageキーにまとめて書き出すため、行数と1行あたりのサイズが増えるほど容量上限とシリアライズのコストが効いてきます。localOnly版はメモリのみで、リロードで消えます。保存先ごとの容量や保存期間の差はCookie・localStorage・IndexedDBの違いと使い分け|容量・保存期間・セキュリティ・デメリット比較で整理しています。
SQLite永続化とオフライン取引パッケージの選択
より強い永続化が要るときは、コレクションの種類を変えるのではなく永続化レイヤーを重ねます。persistedCollectionOptionsはElectric・Query・PowerSync・local-onlyのどのアダプタも包んでSQLiteへ永続化するもので、プラットフォーム別に8つのパッケージがあります。ブラウザ向けの@tanstack/browser-db-sqlite-persistenceはWA-SQLiteのOPFSを使い、ほかにReact Native・Expo・Electron・Node.js・Capacitor・Tauri・Cloudflare Durable Objects向けが揃っています。
オフライン中の書き込みを預かるのは@tanstack/offline-transactionsです。IndexedDBまたはlocalStorageの永続アウトボックスに未送信のトランザクションを積み、WebLocksやBroadcastChannelでタブ間のリーダーを決め、接続が戻るとバックオフ付きで再送します。冪等キーを持たせるので再送による二重登録も避けられます。
| 手段 | パッケージ(版) | 保存先 | 向く用途 |
|---|---|---|---|
| localStorageCollection | @tanstack/react-db 0.4.1 | localStorage | 設定値・UI状態 |
| persistedCollectionOptions | @tanstack/browser-db-sqlite-persistence 0.2.23 | SQLite(OPFS) | 既存コレクションの永続化 |
| offline-transactions | @tanstack/offline-transactions 1.0.56 | IndexedDB等のアウトボックス | オフライン中の書き込み |
| rxdbCollection | @tanstack/rxdb-db-collection 0.1.97 | RxDBのストレージ | レプリケーション込みの同期 |
| powerSyncCollection | @tanstack/powersync-db-collection | SQLite | Postgres/MongoDB/MySQL同期 |
RxDBとPowerSyncは、永続化だけでなく同期そのものを外部エンジンに委ねたい場合の選択肢です。RxDBはDexie.jsやIndexedDBやSQLiteなど複数のストレージを選べ、レプリケーションと競合解決を自前で持ちます。PowerSyncは全データをローカルのSQLiteに置く方式で、公式docsは「All data is stored locally in a SQLite database, allowing your app to work without an internet connection.」としています。
注意点は資料の在りかです。永続化とオフラインの2パッケージは公式ドキュメントサイトのナビゲーションに並んでおらず、説明はリポジトリのpackages/db/skills/配下に置かれています。そのファイルが宣言する対象版は0.6.17のままで、本体の0.9.2とは開きがあります。挙動を確定させたいときはパッケージのソースまで当たる前提で工数を見てください。
導入を見送るべき条件
使うべきでない場面ははっきりしています。第一に、バージョンを固定して定期的に追随する体制が取れないプロジェクトです。0.9.0でも公開APIが削除されており、BETAのまま更新が続いています。第二に、オフライン動作が要件に入っていて、永続化パッケージの仕様をソースから確認する工数が取れない場合です。機能そのものは揃っていますが、前述のとおり追跡コストが乗ります。第三に、データ取得が画面ごとに1エンドポイントで完結し、結合も複数ビューも要らない画面です。得られるのはライブクエリの記法だけで、whereの書き換えコストと依存の増加に見合いません。
エンドポイントの増殖と楽観的更新のキャッシュ整合に実際に困っているなら、TanStack Routerとは?型安全ルーティングの特徴・導入手順とReact Routerとの違いなど他のTanStack製品と同じく、既存構成を壊さず一部から試せます。読み取りだけDBに寄せ、書き込みは既存処理のままutils.refetch()で待つ構成が安全な入口です。
よくある質問
TanStack DBはTanStack Queryの置き換えですか?
いいえ。公式Overviewは拡張と位置づけています。queryCollectionOptionsに既存のqueryKeyとqueryFnをそのまま渡す設計で、取得処理を書き直す必要はありません。
IndexedDBにデータを保存できますか?
IndexedDBに直接読み書きするコレクションはありません。@tanstack/react-db 0.4.1のローカル向けはlocalStorageCollectionOptionsとlocalOnlyCollectionOptionsです。永続化を強めるならpersistedCollectionOptionsでSQLite層を重ねる、オフライン書き込みのアウトボックスとしてIndexedDBを使うなら@tanstack/offline-transactions、RxDBのストレージとして使うならrxdbCollectionOptionsという順で検討します。
localStorageコレクションに業務データを入れても大丈夫ですか?
公式docsの想定は「small amounts of local-only state」で、ユーザー設定や表示状態の保持が用途です。コレクション全体を単一のlocalStorageキーに書き出すため、行数と1行のサイズが増えると容量上限とシリアライズのコストが問題になります。量が読めないデータはpersistedCollectionOptionsのSQLite層に寄せてください。
ElectricSQL以外の同期エンジンとも組めますか?
組めます。公式に用意されているコレクションはElectricSQLのほか、TrailBase、PowerSync、RxDBです。いずれもCollectionインターフェースを実装したパッケージで、独自の同期基盤に合わせて自作することもできます。
本番環境で使って問題ありませんか?
READMEはBETAと明記しており、@tanstack/dbは0.9.2です。0.9.0でも公開APIの削除があったため、採用するなら版を固定し、CHANGELOGを追って更新する運用が前提になります。