meta-framework
compositionIntegrating TanStack DB with meta-frameworks (TanStack Start, Next.js, Remix, Nuxt, SvelteKit). Client-side only: SSR is NOT supported — routes must disable SSR. Preloading collections in route loaders with collection.preload(). Pattern: ssr: false + await collection.preload() in loader. Multiple collection preloading with Promise.all. Framework-specific loader APIs.
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
- Call collection.preload() in the route loader
- Use useLiveQuery in the component
TanStack Start
Global SSR disable
// start.tsx
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>
)
}
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
// app/todos/page.tsx
'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)
// app/todos/page.tsx
'use client'
import { useEffect } from 'react'
import { useLiveQuery } from '@tanstack/react-db'
// Trigger preload immediately when module is loaded
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
// app/routes/todos.tsx
import { useLiveQuery } from '@tanstack/react-db'
import type { ClientLoaderFunctionArgs } from '@remix-run/react'
export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
await todoCollection.preload()
return null
}
// Prevent server loader from running
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:
// src/routes/todos/+page.ts
export const ssr = false
Core Patterns
What preload() does
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.
Collection module pattern
Define collections in a shared module, import in both loaders and components:
// lib/collections.ts
import { createCollection, queryCollectionOptions } from '@tanstack/react-db'
export const todoCollection = createCollection(
queryCollectionOptions({ ... })
)
// routes/todos.tsx — loader uses the same collection instance
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 }))
// ...
},
})
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
Wrong:
// routes/todos.tsx
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:
// lib/collections.ts — single shared instance
export const todoCollection = createCollection(queryCollectionOptions({ ... }))
Collections are singletons. Creating multiple instances for the same data causes duplicate syncs, wasted bandwidth, and inconsistent state between components.
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.