@tanstack/db
Version:
A reactive client store for building super fast apps on sync
362 lines (278 loc) • 8.73 kB
Markdown
---
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 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.
type: composition
library: db
library_version: '0.5.30'
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 collections are **client-side only**. SSR is not implemented. Routes using TanStack DB **must disable SSR**. The setup pattern is:
1. Set `ssr: false` on the route
2. Call `collection.preload()` in the route loader
3. Use `useLiveQuery` in the component
```ts
// start.tsx
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => {
return {
defaultSsr: false,
}
})
```
```tsx
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>
)
}
```
```tsx
export const Route = createFileRoute('/electric')({
ssr: false,
loader: async () => {
await Promise.all([todoCollection.preload(), configCollection.preload()])
return null
},
component: ElectricPage,
})
```
```tsx
// 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)
```tsx
// 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>
)
}
```
```tsx
// 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>
)
}
```
```vue
<!-- 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.
```svelte
<!-- 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>
{
{
<li>{todo.text}</li>
{/each}
{/if}
```
Or disable SSR for the route:
```ts
// src/routes/todos/+page.ts
export const ssr = false
```
`collection.preload()` starts the sync process and returns a promise that resolves when the collection reaches "ready" status. This means:
1. The sync function connects to the backend
2. Initial data is fetched and written to the collection
3. `markReady()` is called by the adapter
4. The promise resolves
Subsequent calls to `preload()` on an already-ready collection return immediately.
Define collections in a shared module, import in both loaders and components:
```ts
// lib/collections.ts
import { createCollection, queryCollectionOptions } from '@tanstack/react-db'
export const todoCollection = createCollection(
queryCollectionOptions({ ... })
)
```
```tsx
// 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 }))
// ...
},
})
```
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](../db-core/collection-setup/references/electric-adapter.md)
- **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:
```tsx
export const Route = createFileRoute('/todos')({
loader: async () => {
await todoCollection.preload()
return null
},
})
```
Correct:
```tsx
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
Wrong:
```tsx
export const Route = createFileRoute('/todos')({
ssr: false,
component: TodoPage,
})
```
Correct:
```tsx
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.
Wrong:
```tsx
// 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:
```ts
// 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.