@tanstack/db

A reactive client store for building super fast apps on sync

meta-framework

composition
362 linesSource

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.

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:

  1. Set ssr: false on the route
  2. Call collection.preload() in the route loader
  3. Use useLiveQuery in the component

TanStack Start

Global SSR disable

ts
// start.tsx
import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() => {
  return {
    defaultSsr: false,
  }
})

Per-route SSR disable + preload

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>
  )
}

Multiple collection preloading

tsx
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

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>
  )
}

Remix

Client loader pattern

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>
  )
}

Nuxt

Client-only component

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.

SvelteKit

Client-side only page

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>

{#if todosQuery}
  {#each todosQuery.data as todo (todo.id)}
    <li>{todo.text}</li>
  {/each}
{/if}

Or disable SSR for the route:

ts
// 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:

  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.

Collection module pattern

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 }))
    // ...
  },
})

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:

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

HIGH Forgetting to preload in route loader

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.

MEDIUM Creating separate collection instances

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.