TanStack / TanStack/db

useLiveQuery blocks rendering of locally persisted collection when Electric is unavailable

Open
#1,416 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.9k
Forks
266
Avg merge
1d 4h
Merged PRs (30d)
55

Description

  • I've validated the bug against the latest version of DB packages

Disclaimer: I'm new to TanStack DB and not very familiar with the codebase. The following analysis and most of the writing were done by GPT-5.4. Feel free to consider this a feature request rather than a bug report.

Expectation

We have an Electric-backed collection with browser SQLite persistence. Our expectation is:

  • if local persisted rows already exist, they should render immediately
  • remote sync can continue in the background
  • if Electric is down, the app should still show the persisted rows

What happens instead is that useLiveQuery blocks the UI.

Versions

We are using TanStack DB with:

  • @tanstack/db@0.6.0
  • @tanstack/solid-db@0.2.14
  • @tanstack/electric-db-collection@0.2.42
  • @tanstack/browser-db-sqlite-persistence@0.1.4

Problem 1: ordered live queries block on query sync

If we create a derived live query with orderBy(...), persisted local rows are not used as initial render data.
In @tanstack/db/src/query/effect.ts:811-815, ordered aliases explicitly disable initial state:

// Ordered aliases explicitly disable initial state — data is loaded
// via requestLimitedSnapshot/requestSnapshot after subscription setup.
if (orderByInfo) {
  return { includeInitialState: false, whereExpression }
}

And in @tanstack/db/src/query/live/collection-config-builder.ts:998-1006, the live query is only marked ready after the full query sync path completes:

// Mark ready when:
// 1. All subscriptions are set up (subscribedToAllCollections)
// 2. All source collections are ready
// 3. The live query collection is not loading subset data
// This prevents marking the live query ready before its data is processed
// (fixes issue where useLiveQuery returns isReady=true with empty data)
if (subscribedToAll && allReady && !isLoading) {
  markReady()
}

So an ordered live query waits for remote/query sync even when the base collection already has persisted local rows.

Problem 2: even the base collection blocks if query() is used

We then tried useLiveQuery(() => persistedBaseCollection) directly, without an ordered derived query.

That still blocks rendering when Electric is down.

In @tanstack/solid-db/src/useLiveQuery.ts:361, the hook waits on currentCollection.toArrayWhenReady().
At the same time, the hook also subscribes to changes and fills query.state reactively. This means:

  • query.state can already contain locally persisted rows
  • but query() still suspends because it waits for toArrayWhenReady()

In practice, our Solid UI behaves like this when Electric is down:

  1. layout appears briefly
  2. query() suspends
  3. UI below that point disappears
  4. persisted rows are not shown

The only workaround we found is to avoid query() and render from query.state manually.

Expected behavior

If local persisted data exists, there should be an official way to render it immediately without waiting for remote readiness.

Remote sync can still remain in loading state separately.

What would help

Any of these would solve the issue for us:

  1. A local-first mode for useLiveQuery

    • return local persisted rows immediately if available
    • keep sync status as loading until remote readiness
  2. A non-suspending accessor in addition to query()

    • something like query.localData / query.currentData
    • officially supported, unlike reading query.state directly
  3. Configurable ordered-query behavior

    • allow orderBy(...) queries to use local initial state instead of always blocking on query sync
    • even if this is opt-in

Why this matters

Persistence is much less useful if persisted rows cannot actually be rendered when the sync source is unavailable.

Right now, the only workable approach for us is:

  • avoid ordered derived live queries for offline rendering
  • avoid query()
  • read from query.state directly

That feels like a workaround rather than the intended API.

Minimal example

import {
  createBrowserWASQLitePersistence,
  persistedCollectionOptions,
} from '@tanstack/browser-db-sqlite-persistence';
import {
  electricCollectionOptions,
  type ElectricCollectionUtils,
} from '@tanstack/electric-db-collection';
import { createCollection, useLiveQuery } from '@tanstack/solid-db';
import { For } from 'solid-js';

import { apiBaseUrl } from './api';
// Use the local OPFS helper because the published browser package currently hardcodes
// an absolute worker asset path that breaks in Vite dev.
import { openBrowserWASQLiteOPFSDatabase } from './utility/opfs-database';

type Chat = {
  id: string;
  title: string;
  updated_at: string;
};

const database = await openBrowserWASQLiteOPFSDatabase({
  databaseName: 'example.sqlite',
});
const persistence = createBrowserWASQLitePersistence<Chat, string | number>({ database });
const chatsCollection = createCollection(
  persistedCollectionOptions<Chat, string | number, never, ElectricCollectionUtils<Chat>>({
    persistence,
    schemaVersion: 1,
    ...electricCollectionOptions<Chat>({
      id: 'chats',
      shapeOptions: {
        url: `${apiBaseUrl}/electric/chats`,
      },
      getKey: (chat) => chat.id,
    }),
  }),
);

export function OrderedExample() {
  const chatsQuery = useLiveQuery((q) =>
    q.from({ chats: chatsCollection }).orderBy(({ chats }) => chats.updated_at, 'desc'),
  );

  return (
    <>
      <p>OrderedExample</p>
      <ul>
        <For each={chatsQuery()}>{(chat) => <li>{chat.title}</li>}</For>
      </ul>
    </>
  );
}

export function BaseCollectionExample() {
  const chatsQuery = useLiveQuery(() => chatsCollection);

  return (
    <>
      <p>BaseCollectionExample</p>
      <ul>
        <For each={chatsQuery()}>{(chat) => <li>{chat.title}</li>}</For>
      </ul>
    </>
  );
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/db/src/query/effect.ts, packages/db/src/query/live/collection-config-builder.ts, and packages/solid-db/src/useLiveQuery.ts, then reproduce the ordered and base-collection examples with Electric unavailable. Trace how initial state, readiness, and toArrayWhenReady() interact with persisted rows. Done means an official accessor or mode renders local persisted data immediately while remote sync remains loading.

Written by the indexing model from the issue text.

Assessment

Tech stack
sqlite, typescript
Domain
databases, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.