fix(react-db): defer eager onStoreChange to a microtask in useLiveQuery#1588
fix(react-db): defer eager onStoreChange to a microtask in useLiveQuery#1588tsushanth wants to merge 1 commit into
Conversation
Closes TanStack#1587. `useLiveQuery`'s `subscribeRef` calls `onStoreChange()` synchronously inside the `useSyncExternalStore` subscribe function when the underlying collection is already `ready`. That synchronous notification lands during the render-to-commit window when subscribe runs under StrictMode double-render or cold/throttled loads, which React surfaces as: Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. The fix is to defer the eager notification to a microtask so it lands after the current commit. While doing so, also guard the late notify path against an in-flight `subscribeChanges` callback firing after React unsubscribes — track a local `unsubscribed` flag and drop both the eager microtask and any in-flight subscription event after teardown, so React never sees a state update post-unsubscribe. No public API change; the contract of `useLiveQuery` is preserved (an already-ready collection still notifies React once after mount, just asynchronously instead of mid-commit). Verified `pnpm test` in packages/react-db — 94/94 pass, no type errors. Existing tests don't cover the race directly (it's a StrictMode-double-render / cold-load condition observed via Lighthouse in the issue), so the existing suite is the regression guard for existing behavior and the issue's repro is the behavioral validation.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesSubscription race condition fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Closes #1587.
useLiveQuery'ssubscribeRefcallsonStoreChange()synchronously inside theuseSyncExternalStoresubscribe function when the underlying collection is alreadyready:That synchronous notification lands during the render-to-commit window when subscribe runs under StrictMode double-render or cold/throttled loads, which React surfaces as:
The reporter observed this consistently in a Lighthouse-cold-desktop run with React 19 +
@tanstack/react-db@0.1.86+@tanstack/db@0.6.8.Fix
Defer the eager notification to a microtask so it lands after the current commit:
While doing so, also guard the late-notify paths against an in-flight
subscribeChangescallback firing after React unsubscribes. Track a localunsubscribedflag set by the cleanup, and drop both the eager microtask and any in-flight subscription event after teardown, so React never sees a state update post-unsubscribe:API impact
None. The contract of
useLiveQueryis preserved — an already-ready collection still notifies React once after mount, just asynchronously instead of mid-commit. Callers observe identical state transitions; the only difference is the timing of the first notification when the collection is already warm at hook-init.Test plan
pnpm testinpackages/react-db— 94/94 pass, no type errors (existing suite unchanged)useLiveQuery.tsis 96.47% lines / 95.65% branches after the change; the new microtask branch is hit by the existing tests that exercise the ready-collection pathuseSyncExternalStorecontract regardless of reproNotes for review
Promise.resolve().then(...)vsqueueMicrotask(...). Both land in the same microtask queue, butqueueMicrotaskis the more semantically explicit "do this after the current sync work" primitive. Either works.useEffect-based subscription, which the React warning message suggests, because the existing architecture commits touseSyncExternalStore. The microtask deferral is the minimal, contract-faithful fix.Summary by CodeRabbit