Skip to content
cccskills
FreePublished on GitHub

tanstack-db

Octane bindings for TanStack DB. useLiveQuery hook with dependency arrays (8 overloads: query function, config object, pre-created collection, disabled state via returning undefined/null). useLiveSuspenseQuery for Octane Suspense with Error Boundaries (data always defined). useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage, hasNextPage, isFetchingNextPage). usePacedMutations for debounced Octane state updates. Return shape: data, state, collection, status, isLoading, isReady, isError. Import from @octanejs/tanstack-db (re-exports all of @tanstack/db).

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md10.6 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.

TanStack DB — Octane

Setup

import { useLiveQuery, eq, not } from '@octanejs/tanstack-db'

function TodoList() {
  const { data: todos, isLoading } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => not(todo.completed))
      .orderBy(({ todo }) => todo.created_at, 'asc'),
  )

  if (isLoading) return <div>Loading...</div>

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

@octanejs/tanstack-db re-exports everything from @tanstack/db. In Octane projects, import everything from @octanejs/tanstack-db.

Hooks

useLiveQuery

// Query function with dependency array
const {
  data,
  state,
  collection,
  status,
  isLoading,
  isReady,
  isError,
  isIdle,
  isCleanedUp,
} = useLiveQuery(
  (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.userId, userId)),
  [userId],
)

// Config object
const { data } = useLiveQuery({
  query: (q) => q.from({ todo: todoCollection }),
  gcTime: 60000,
})

// Pre-created collection (from route loader)
const { data } = useLiveQuery(preloadedCollection)

// Conditional query — return undefined/null to disable
const { data, status } = useLiveQuery(
  (q) => {
    if (!userId) return undefined
    return q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.userId, userId))
  },
  [userId],
)
// When disabled: status='disabled', data=undefined

useLiveSuspenseQuery

// data is ALWAYS defined — never undefined
// Must wrap in <Suspense> and <ErrorBoundary>
function TodoList() {
  const { data: todos } = useLiveSuspenseQuery((q) =>
    q.from({ todo: todoCollection }),
  )

  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>{t.text}</li>
      ))}
    </ul>
  )
}

// With deps — re-suspends when deps change
const { data } = useLiveSuspenseQuery(
  (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.category, category)),
  [category],
)

useLiveInfiniteQuery

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
  useLiveInfiniteQuery(
    (q) =>
      q
        .from({ posts: postsCollection })
        .where(({ posts }) => eq(posts.category, category))
        .orderBy(({ posts }) => posts.createdAt, 'desc'),
    { pageSize: 20 },
    [category],
  )

// data is the flat array of all loaded pages
// fetchNextPage() loads the next page
// hasNextPage is true when more data is available

usePacedMutations

import { usePacedMutations, debounceStrategy } from "@octanejs/tanstack-db"

const mutate = usePacedMutations({
  onMutate: (value: string) => {
    noteCollection.update(noteId, (draft) => {
      draft.content = value
    })
  },
  mutationFn: async ({ transaction }) => {
    await api.notes.update(noteId, transaction.mutations[0].changes)
  },
  strategy: debounceStrategy({ wait: 500 }),
})

// In handler:
<textarea onChange={(e) => mutate(e.target.value)} />

useLiveQueryEffect

import { useLiveQueryEffect, eq } from '@octanejs/tanstack-db'

// Fire side effects when rows enter, exit, or update a query result.
// No render output — the effect is created on mount, disposed on unmount,
// and recreated when deps change.
function ChatComponent() {
  useLiveQueryEffect(
    {
      query: (q) =>
        q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),
      skipInitial: true,
      onEnter: async (event) => {
        await generateResponse(event.value)
      },
    },
    [],
  )

  return <div>...</div>
}

Includes (Hierarchical Data)

When a query uses includes (subqueries in select), each child field is a live Collection by default. Subscribe to it with useLiveQuery in a subcomponent:

function ProjectList() {
  const { data: projects } = useLiveQuery((q) =>
    q.from({ p: projectsCollection }).select(({ p }) => ({
      id: p.id,
      name: p.name,
      issues: q
        .from({ i: issuesCollection })
        .where(({ i }) => eq(i.projectId, p.id))
        .select(({ i }) => ({ id: i.id, title: i.title })),
    })),
  )

  return (
    <ul>
      {projects.map((project) => (
        <li key={project.id}>
          {project.name}
          <IssueList issuesCollection={project.issues} />
        </li>
      ))}
    </ul>
  )
}

// Child component subscribes to the child Collection
function IssueList({ issuesCollection }) {
  const { data: issues } = useLiveQuery(issuesCollection)
  return (
    <ul>
      {issues.map((issue) => (
        <li key={issue.id}>{issue.title}</li>
      ))}
    </ul>
  )
}

Only the affected IssueList re-renders when an issue changes — the parent does not.

With toArray(), child results are plain arrays and the parent re-renders on child changes:

import { toArray, eq } from '@octanejs/tanstack-db'

const { data: projects } = useLiveQuery((q) =>
  q.from({ p: projectsCollection }).select(({ p }) => ({
    id: p.id,
    name: p.name,
    issues: toArray(
      q
        .from({ i: issuesCollection })
        .where(({ i }) => eq(i.projectId, p.id))
        .select(({ i }) => ({ id: i.id, title: i.title })),
    ),
  })),
)
// project.issues is Array<{ id, title }> — no subcomponent needed

See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).

Virtual Properties

Live query results include computed, read-only virtual properties on every row:

  • $synced: true when the row is confirmed by sync; false when it is still optimistic.
  • $origin: "local" if the last confirmed change came from this client, otherwise "remote".
  • $key: the row key for the result.
  • $collectionId: the source collection ID.

These props are added automatically and can be used in where, select, and orderBy clauses. Do not persist them back to storage.

const { data } = useLiveQuery(
  (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.$synced, false)),
  [],
)
// Shows only optimistic (unconfirmed) todos

Octane-Specific Patterns

Dependency arrays

// Include ALL external reactive values
const { data } = useLiveQuery(
  (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) =>
        and(eq(todo.userId, userId), eq(todo.status, filter)),
      ),
  [userId, filter],
)

// Empty array = static query, never re-runs
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }), [])

// No array = re-runs on every render (usually wrong)

Suspense + Error Boundary

<ErrorBoundary fallback={<div>Error</div>}>
  <Suspense fallback={<div>Loading...</div>}>
    <TodoList />
  </Suspense>
</ErrorBoundary>

Router loader preloading

// In route loader:
await todoCollection.preload()

// In component — data available immediately:
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))

See meta-framework/SKILL.md for full preloading patterns.

Common Mistakes

CRITICAL Missing external values in dependency array

Wrong:

const { data } = useLiveQuery((q) =>
  q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.userId, userId)),
)

Correct:

const { data } = useLiveQuery(
  (q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.userId, userId)),
  [userId],
)

When the query uses external state not in the deps array, the query won't re-run when that value changes, showing stale results.

Source: docs/framework/octane/overview.md

HIGH useLiveSuspenseQuery without Error Boundary

Wrong:

<Suspense fallback={<div>Loading...</div>}>
  <TodoList /> {/* uses useLiveSuspenseQuery */}
</Suspense>

Correct:

<ErrorBoundary fallback={<div>Error</div>}>
  <Suspense fallback={<div>Loading...</div>}>
    <TodoList />
  </Suspense>
</ErrorBoundary>

useLiveSuspenseQuery throws errors during rendering. Without an Error Boundary, the entire app crashes.

Source: docs/guides/live-queries.md

HIGH "Not a Collection" error from duplicate @tanstack/db

If useLiveQuery throws InvalidSourceError: The value provided for alias "todo" is not a Collection, it usually means two copies of @tanstack/db are installed. The collection was created by one copy, but useLiveQuery checks instanceof against the other.

In dev mode, TanStack DB also throws DuplicateDbInstanceError if two instances are detected.

Diagnose:

pnpm ls @tanstack/db

If multiple versions appear, fix with one of:

pnpm overrides (in root package.json):

{
  "pnpm": {
    "overrides": {
      "@tanstack/db": "^0.6.0"
    }
  }
}

Vite resolve.alias (in vite.config.ts):

import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@tanstack/db': path.resolve('./node_modules/@tanstack/db'),
    },
  },
})

The root cause is typically a dependency that bundles its own copy instead of declaring @tanstack/db as a peerDependency.

HIGH Tension: Query expressiveness vs. IVM constraints

The query builder looks like SQL but has constraints that SQL doesn't — equality joins only, orderBy required for limit/offset, no distinct without select. Agents write SQL-style queries that violate these constraints. See db-core/live-queries/SKILL.md § Common Mistakes for all constraints.

See also: db-core/live-queries/SKILL.md — for query builder API and all operators.

See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.

See also: meta-framework/SKILL.md — for preloading in route loaders.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Write a new Octane component in .tsrx, convert JSX to .tsrx, or add scoped <style> blocks and themes. Use when creating a .tsrx file rather than editing one, because the path-scoped tsrx-authoring rule only fires once such a file is open.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

Find and fix a bug in the octane monorepo. Use when given a failing test, a regression, or a report that behavior differs from expectation, including deciding whether a React mismatch is an intentional divergence.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

Keep a change small and in the existing idiom - size the plan and weigh smaller alternatives before writing it, reuse the mechanism that already owns the behavior instead of adding a parallel one, and simplify the diff before handoff. Use when planning any change beyond a few lines, before readying a PR, or when reviewing agent-written code for bloat.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

Open a pull request for octane work. Load before creating the branch, commit, changeset, or PR, including when opening a PR is the tail step of a task that was about something else.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

Work a GitHub issue in the octane repo. Use when given an issue number or link. Fetches the issue, classifies the owning area, and plans the fix.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

Change Octane's runtime, compiler, scheduler, reconciler, SSR, or hydration engine. Use before editing packages/octane/src. Covers the observable contract, hot-path analysis, and the performance evidence required before handoff.

The English summary is being prepared. Showing the original description.

octanejs/octane1.5KUpdated Oct 20, 2026

See all skills by octanejs

Report a problem with this skill