-
Notifications
You must be signed in to change notification settings - Fork 87
Description
Background
Currently, query-db-collection
creates a single QueryObserver that stays subscribed to TanStack Query for the entire lifecycle of the collection. This breaks TanStack Query's staleTime behavior because from Query's perspective, there's always an "active" observer.
The Problem
Expected behavior (like useQuery):
- Component mounts → Query becomes active
- Component unmounts → Query becomes inactive, staleTime timer starts
- Component remounts within staleTime → No refetch occurs
- Component remounts after staleTime → Refetch occurs
Current behavior in query-db-collection:
- Collection starts sync → QueryObserver subscribes and stays subscribed
- Query is ALWAYS active from TanStack Query's perspective
- staleTime never applies because query never goes "inactive"
- refetchOnWindowFocus always triggers regardless of staleTime
Evidence
From query.ts:303-332:
const internalSync: SyncConfig<TItem>['sync'] = (params) => {
// Creates observer when collection starts sync
const localObserver = new QueryObserver(queryClient, observerOptions)
// Subscribes immediately and stays subscribed
const actualUnsubscribeFn = localObserver.subscribe((result) => {
// ... sync logic
})
// Only unsubscribes when collection is cleaned up
return async () => {
actualUnsubscribeFn()
// ...
}
}
Root Cause
Collections already track subscriber count (lines 663-685 in collection.ts), but query-db-collection doesn't use this information to manage its QueryObserver lifecycle.
Proposed Solution
Modify query-db-collection to subscribe/unsubscribe its QueryObserver based on the collection's subscriber count:
const internalSync: SyncConfig<TItem>['sync'] = (params) => {
const { collection } = params
let localObserver: QueryObserver | null = null
let queryUnsubscribe: (() => void) | null = null
// Track when collection gains/loses subscribers
const originalAddSubscriber = collection.addSubscriber
const originalRemoveSubscriber = collection.removeSubscriber
// Override to manage QueryObserver lifecycle
collection.addSubscriber = function() {
const wasInactive = this.activeSubscribersCount === 0
originalAddSubscriber.call(this)
// Create QueryObserver when first subscriber added
if (wasInactive && \!localObserver) {
localObserver = new QueryObserver(queryClient, observerOptions)
queryUnsubscribe = localObserver.subscribe((result) => {
// ... existing sync logic
})
}
}
collection.removeSubscriber = function() {
originalRemoveSubscriber.call(this)
// Cleanup QueryObserver when last subscriber removed
if (this.activeSubscribersCount === 0 && localObserver) {
queryUnsubscribe?.()
localObserver = null
queryUnsubscribe = null
// Now TanStack Query's staleTime timer can start
}
}
return async () => {
// Restore original methods
collection.addSubscriber = originalAddSubscriber
collection.removeSubscriber = originalRemoveSubscriber
// Final cleanup
queryUnsubscribe?.()
await queryClient.cancelQueries({ queryKey })
queryClient.removeQueries({ queryKey })
}
}
Alternative Approach
Instead of overriding methods, collections could expose subscriber count changes via events:
// In collection.ts
private notifySubscriberCountChange(count: number) {
this.emit('subscriberCountChanged', count)
}
// In query-db-collection
collection.on('subscriberCountChanged', (count) => {
if (count === 0 && localObserver) {
// Unsubscribe QueryObserver
} else if (count === 1 && \!localObserver) {
// Subscribe QueryObserver
}
})
Benefits
- Proper staleTime behavior: Data goes stale when no components are using it
- Better performance: Avoids unnecessary refetches within staleTime window
- Memory efficiency: Queries can be garbage collected after gcTime
- TanStack Query compatibility: Behaves like regular useQuery hooks
Test Cases
- Component mounts → Query should become active, fetch if stale
- Component unmounts → Query should become inactive after delay
- Component remounts within staleTime → Should not refetch
- Component remounts after staleTime → Should refetch
- Multiple components → Query stays active until all unmount
- Window focus within staleTime → Should not refetch
- Window focus after staleTime → Should refetch
Breaking Changes
This should be backwards compatible - existing behavior is preserved, but staleTime will now work correctly.