Skip to content

Instantly share code, notes, and snippets.

@MAST1999
Created September 20, 2026 03:45
Show Gist options
  • Select an option

  • Save MAST1999/c2261d268a68610f18c5a6c45525b468 to your computer and use it in GitHub Desktop.

Select an option

Save MAST1999/c2261d268a68610f18c5a6c45525b468 to your computer and use it in GitHub Desktop.
Repro: TanStack DB useLiveSuspenseQuery never releases over an on-demand collection with an async loadSubset

Repro: useLiveSuspenseQuery never releases over an on-demand collection with an async loadSubset

@tanstack/db@0.9.2 · @tanstack/react-db@0.4.1 (latest at time of writing)

npm install
npx vitest run     # real Chromium, via @vitest/browser + playwright

Plain @tanstack/db. No persistence, no Electric, no orderBy, no limit. The only variable is whether loadSubset answers with the synchronous true that LoadSubsetFn allows, or with a Promise. Both deliver the same rows.

✓ a synchronous loadSubset releases the boundary                      54ms
× an async loadSubset resolving after   0ms   → text="loading" renders=23525
× an async loadSubset resolving after  50ms   → text="loading" renders=416
× an async loadSubset resolving after 500ms   → text="loading" renders=56
× an explicit queryKey does not change the outcome
× StrictMode is not the cause

The render count falling as the delay grows is the signature of a re-suspend cycle rather than a busy loop: each retry waits for the promise, so a slower promise simply fits fewer retries into the same budget.

{
"name": "tanstack-db-suspense-ondemand-repro",
"private": true,
"type": "module",
"scripts": { "test": "vitest run" },
"dependencies": {
"@tanstack/db": "0.9.2",
"@tanstack/react-db": "0.4.1",
"react": "^19",
"react-dom": "^19"
},
"devDependencies": { "@vitest/browser": "^3", "playwright": "^1.56", "vitest": "^3" }
}
/**
* `useLiveSuspenseQuery` never releases over an `on-demand` collection whose
* `loadSubset` returns a Promise.
*
* npm install && npx vitest run # real Chromium
*
* The collection is plain `@tanstack/db` — no persistence, no Electric, no
* `orderBy`, no `limit`. The only variable is whether `loadSubset` answers
* with the synchronous `true` that `LoadSubsetFn` allows, or with a Promise.
*/
import { createCollection, eq } from "@tanstack/db";
import { useLiveSuspenseQuery } from "@tanstack/react-db";
import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { expect, test } from "vitest";
const ROWS = [
{ id: `1`, listId: `a`, text: `first` },
{ id: `2`, listId: `a`, text: `second` },
];
/**
* `mode: "sync"` answers `true`; `mode: "async"` resolves after `delayMs`.
* Both deliver exactly the same rows.
*/
const makeCollection = ({ mode, delayMs = 0 }) =>
createCollection({
id: `items-${mode}-${delayMs}`,
syncMode: `on-demand`,
getKey: (row) => row.id,
sync: {
sync: ({ begin, write, commit, markReady }) => {
begin();
commit();
markReady();
const deliver = () => {
begin();
for (const row of ROWS) write({ type: `insert`, value: row });
commit();
};
return {
loadSubset: () => {
if (mode === `sync`) {
deliver();
return true;
}
return new Promise((resolve) => {
setTimeout(() => {
deliver();
resolve();
}, delayMs);
});
},
};
},
},
});
const probe = async ({ mode, delayMs = 0, strict = true, queryKey = false }) => {
const collection = makeCollection({ mode, delayMs });
const renders = { count: 0 };
const List = () => {
renders.count++;
const query = (q) => q.from({ row: collection }).where(({ row }) => eq(row.listId, `a`));
const { data } = useLiveSuspenseQuery(
queryKey ? { queryKey: [mode, delayMs], query } : { query },
);
return <span>rows:{data.length}</span>;
};
const host = document.createElement(`div`);
document.body.appendChild(host);
const root = createRoot(host);
const tree = (
<Suspense fallback={<span>loading</span>}>
<List />
</Suspense>
);
root.render(strict ? <StrictMode>{tree}</StrictMode> : tree);
const deadline = Date.now() + 5_000;
while (Date.now() < deadline && !host.textContent.startsWith(`rows:`)) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
const result = { text: host.textContent, renders: renders.count };
root.unmount();
host.remove();
return result;
};
test(`a synchronous loadSubset releases the boundary`, async () => {
const result = await probe({ mode: `sync` });
expect(result.text).toBe(`rows:2`);
expect(result.renders).toBeLessThan(10);
}, 12_000);
// Everything below is the same collection with an async `loadSubset`.
for (const delayMs of [0, 50, 500]) {
test(
`an async loadSubset resolving after ${delayMs}ms releases the boundary`,
async () => {
const result = await probe({ mode: `async`, delayMs });
expect({ text: result.text, renders: result.renders }).toEqual({
text: `rows:2`,
renders: result.renders,
});
expect(result.renders).toBeLessThan(100);
},
12_000,
);
}
// Two things a reviewer would reasonably suspect, ruled out.
test(`an explicit queryKey does not change the outcome`, async () => {
const result = await probe({ mode: `async`, delayMs: 500, queryKey: true });
expect(result.text).toBe(`rows:2`);
}, 12_000);
test(`StrictMode is not the cause`, async () => {
const result = await probe({ mode: `async`, delayMs: 500, strict: false });
expect(result.text).toBe(`rows:2`);
}, 12_000);
import { defineConfig } from "vitest/config";
export default defineConfig({
esbuild: { jsx: "automatic" },
test: {
include: ["*.test.jsx"],
browser: { enabled: true, provider: "playwright", headless: true, instances: [{ browser: "chromium" }] },
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment