| name | meta-framework |
| description | Integrating TanStack DB with meta-frameworks (TanStack Start, Next.js, Remix, Nuxt, SvelteKit). Client-side only: SSR is NOT supported — routes must disable SSR. Preloading eager collections in route loaders with collection.preload(). On-demand Query Collections require preloading the live query because source collection preload is a no-op. Multiple collection preloading with Promise.all. Framework-specific loader APIs.
|
| type | composition |
| library | db |
| library_version | 0.6.17 |
| requires | ["db-core","db-core/collection-setup"] |
| sources | ["TanStack/db:examples/react/todo/src/routes/electric.tsx","TanStack/db:examples/react/todo/src/routes/query.tsx","TanStack/db:examples/react/todo/src/start.tsx"] |
This skill builds on db-core. Read it first for collection setup and query builder.
TanStack DB — Meta-Framework Integration
Setup
TanStack DB collections are client-side only. SSR is not implemented. Routes using TanStack DB must disable SSR. The setup pattern is:
- Set
ssr: false on the route
- Preload the eager collection, or preload the live query for an on-demand source
- Use
useLiveQuery in the component
TanStack Start
Global SSR disable
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => {
return {
defaultSsr: false,
}
})
Per-route SSR disable + preload
import { createFileRoute } from '@tanstack/react-router'
import { useLiveQuery } from '@tanstack/react-db'
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => {
await todoCollection.preload()
return null
},
component: TodoPage,
})
function TodoPage() {
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
)
}
On-demand Query Collection preload
Calling preload() on an on-demand source collection is a no-op. Define the
live query once, preload it in the loader, and pass that same collection to the
framework hook:
import { createLiveQueryCollection, eq } from '@tanstack/db'
import { useLiveQuery } from '@tanstack/react-db'
const activeTodos = createLiveQueryCollection((q) =>
q
.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false)),
)
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => {
await activeTodos.preload()
return null
},
component: () => {
const { data } = useLiveQuery(activeTodos)
},
})
Multiple collection preloading
export const Route = createFileRoute('/electric')({
ssr: false,
loader: async () => {
await Promise.all([todoCollection.preload(), configCollection.preload()])
return null
},
component: ElectricPage,
})
Next.js (App Router)
Client component with preloading
'use client'
import { useEffect, useState } from 'react'
import { useLiveQuery } from '@tanstack/react-db'
export default function TodoPage() {
const { data: todos, isLoading } = useLiveQuery((q) =>
q.from({ todo: todoCollection }),
)
if (isLoading) return <div>Loading...</div>
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
)
}
Next.js App Router components using TanStack DB must be client components ('use client'). There is no server-side preloading — collections sync on mount.
With route-level preloading (experimental)
'use client'
import { useEffect } from 'react'
import { useLiveQuery } from '@tanstack/react-db'
const preloadPromise = todoCollection.preload()
export default function TodoPage() {
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
)
}
Remix
Client loader pattern
import { useLiveQuery } from '@tanstack/react-db'
import type { ClientLoaderFunctionArgs } from '@remix-run/react'
export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
await todoCollection.preload()
return null
}
export const loader = () => null
export default function TodoPage() {
const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
)
}
Nuxt
Client-only component
<!-- pages/todos.vue -->
<script setup lang="ts">
import { useLiveQuery } from '@tanstack/vue-db'
const { data: todos, isLoading } = useLiveQuery((q) =>
q.from({ todo: todoCollection }),
)
</script>
<template>
<ClientOnly>
<div v-if="isLoading">Loading...</div>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
</ClientOnly>
</template>
Wrap TanStack DB components in <ClientOnly> to prevent SSR.
SvelteKit
Client-side only page
<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
import { browser } from '$app/environment'
import { useLiveQuery } from '@tanstack/svelte-db'
const todosQuery = browser
? useLiveQuery((q) => q.from({ todo: todoCollection }))
: null
</script>
{#if todosQuery}
{#each todosQuery.data as todo (todo.id)}
<li>{todo.text}</li>
{/each}
{/if}
Or disable SSR for the route:
export const ssr = false
Core Patterns
What preload() does
For eager collections, collection.preload() starts the sync process and
returns a promise that resolves when the collection reaches "ready" status.
This means:
- The sync function connects to the backend
- Initial data is fetched and written to the collection
markReady() is called by the adapter
- The promise resolves
Subsequent calls to preload() on an already-ready collection return immediately.
For on-demand collections, source collection.preload() warns and does
nothing because no subset has been requested. Create the required live query
and await liveQuery.preload().
Stable collection ownership
For one global QueryClient and one global server resource, define the
collection in a shared module and import it in loaders and components:
import { createCollection, queryCollectionOptions } from '@tanstack/react-db'
export const todoCollection = createCollection(
queryCollectionOptions({ ... })
)
import { todoCollection } from '../lib/collections'
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => {
await todoCollection.preload()
return null
},
component: () => {
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
},
})
When the QueryClient, tenant, project, account, or route parameter defines
the resource, create one stable collection per QueryClient and business
scope. Memoize it and put it in router/request context rather than using a
process-global collection. Remove unused entries and call
collection.cleanup() in long-lived scope maps.
See the
Query adapter runtime and business-scope pattern.
Server-Side Integration
This skill covers the client-side read path only (preloading, live queries). For server-side concerns:
- Electric proxy route (forwarding shape requests to Electric) — see the Electric adapter reference
- Mutation endpoints (
createServerFn in TanStack Start, API routes in Next.js/Remix) — implement using your framework's server function pattern. See the Electric adapter reference for the txid handshake that mutations must return.
Common Mistakes
CRITICAL Enabling SSR with TanStack DB
Wrong:
export const Route = createFileRoute('/todos')({
loader: async () => {
await todoCollection.preload()
return null
},
})
Correct:
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => {
await todoCollection.preload()
return null
},
})
TanStack DB collections are client-side only. Without ssr: false, the route loader runs on the server where collections cannot sync, causing hangs or errors.
Source: examples/react/todo/src/start.tsx
HIGH Forgetting to preload in route loader
Wrong:
export const Route = createFileRoute('/todos')({
ssr: false,
component: TodoPage,
})
Correct:
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => {
await todoCollection.preload()
return null
},
component: TodoPage,
})
Without preloading, the collection starts syncing only when the component mounts, causing a loading flash. Preloading in the route loader starts sync during navigation, making data available immediately when the component renders.
MEDIUM Creating separate collection instances in one scope
Wrong:
const todoCollection = createCollection(queryCollectionOptions({ ... }))
export const Route = createFileRoute('/todos')({
ssr: false,
loader: async () => { await todoCollection.preload() },
component: () => {
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
},
})
Correct:
export const todoCollection = createCollection(queryCollectionOptions({ ... }))
Collections are stable within a QueryClient and business scope; they are not
universal singletons. Creating several instances in one scope causes duplicate
syncs and split state. A request-, router-, tenant-, or route-scoped client
needs a scoped factory instead of the global module pattern.
See also: react-db/SKILL.md, vue-db/SKILL.md, svelte-db/SKILL.md, solid-db/SKILL.md, angular-db/SKILL.md — for framework-specific hook usage.
See also: db-core/collection-setup/SKILL.md — for collection creation and adapter selection.