Skip to content

fix(react-db): refresh live query snapshot after subscribe#1516

Open
lalitkapoor wants to merge 2 commits intoTanStack:mainfrom
lalitkapoor:lalitkapoor--react-db-loading-snapshot-refresh
Open

fix(react-db): refresh live query snapshot after subscribe#1516
lalitkapoor wants to merge 2 commits intoTanStack:mainfrom
lalitkapoor:lalitkapoor--react-db-loading-snapshot-refresh

Conversation

@lalitkapoor
Copy link
Copy Markdown

🎯 Changes

Fixes a useLiveQuery subscription timing bug where the client could fail to see rows that were already in the collection.

The failure mode was:

  1. useLiveQuery read a render-time snapshot while the collection was still empty.
  2. Rows were written into the collection before React attached the useSyncExternalStore subscription. In our app, this is the shape of local/persisted rows hydrating while upstream sync is still pending.
  3. Because the collection status was still loading, the hook skipped its post-subscribe refresh.
  4. React kept rendering the stale empty snapshot, even though the collection had rows available. The client did not see those rows until a later collection change or ready transition happened.

The fix is to force one snapshot refresh immediately after subscribing regardless of collection status. That makes rows written during the render-to-subscribe gap visible while isLoading can still remain true for upstream sync.

Adds a regression test with a real collection that writes a row through sync callbacks during that gap and verifies useLiveQuery returns the row while still loading.

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Force one post-subscribe snapshot refresh even when a collection is still loading so rows hydrated between render and subscription attachment are visible immediately.

Add a regression test for the loading collection race and include a react-db patch changeset.
@lalitkapoor lalitkapoor marked this pull request as ready for review May 5, 2026 04:48
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1516

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1516

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1516

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1516

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1516

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1516

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1516

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1516

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1516

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1516

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1516

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1516

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1516

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1516

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1516

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1516

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1516

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1516

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1516

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1516

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1516

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1516

commit: 173cf00

@kevin-dp
Copy link
Copy Markdown
Contributor

kevin-dp commented May 5, 2026

Hi @lalitkapoor,

Thank you for opening this PR.
I like how small the change is, but i do believe it has a number of implications we need to carefully consider (numbers 1 and 2 below). This probably requires some more discussion with @samwillis and @KyleAMathews

Here are the concerns:

1. Extra render on every subscribe (minor performance).
Previously, mounting a hook against a loading collection skipped the refresh. Now every mount/dep-change triggers an extra onStoreChangegetSnapshot → render. Bounded (one per subscribe), almost certainly fine, but worth being aware of in components that mount many useLiveQuery hooks.

2. Unconditional bump even when nothing changed.
If no rows landed during the render→subscribe gap, the bump still forces a re-render with an identical data set (new snapshotRef object, same content). React's reconciliation will short-circuit downstream, but consumers using result as a dependency in useEffect / useMemo could see a referentially-different object even when nothing changed. Probably acceptable, but a behavior change worth calling out in the changeset.

3. Test fragility — write-during-render.
The test performs syncBegin/syncWrite/syncCommit inside the render callback gated by a didWriteAfterSnapshot flag. This works because renderHook invokes the function during render (before effects/subscribe), but it's an unusual pattern that depends on React Testing Library's internals. Two suggestions:

  • Consider doing the write inside a useLayoutEffect of a child or via a more explicit "after render, before commit" hook — or at minimum, comment that the placement is load-bearing.
  • The flag approach risks confusing future maintainers; a comment like "write must happen during render — moving this to an effect breaks the repro" would help.

4. Test coverage gap.
The test verifies the row appears, but doesn't verify the absence of regressions for the existing path:

  • A collection that's already ready at subscribe time — does it still get exactly one refresh (not two)?
  • A subsequent syncWrite after the initial refresh — does normal change propagation still work?

Both are likely fine given how minimal the change is, but a one-line follow-up assertion would lock this in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants