Skip to content

Commit 1b73361

Browse files
authored
use useSyncExternalStore for useLiveQuery (#225)
1 parent 4992c5f commit 1b73361

File tree

2 files changed

+106
-37
lines changed

2 files changed

+106
-37
lines changed

packages/db/src/query/live-query-collection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export interface LiveQueryCollectionConfig<
7979
* Start sync / the query immediately
8080
*/
8181
startSync?: boolean
82+
83+
/**
84+
* GC time for the collection
85+
*/
86+
gcTime?: number
8287
}
8388

8489
/**
@@ -322,6 +327,7 @@ export function liveQueryCollectionOptions<
322327
config.getKey || ((item) => resultKeys.get(item) as string | number),
323328
sync,
324329
compare,
330+
gcTime: config.gcTime || 5000, // 5 seconds by default for live queries
325331
schema: config.schema,
326332
onInsert: config.onInsert,
327333
onUpdate: config.onUpdate,

packages/react-db/src/useLiveQuery.ts

Lines changed: 100 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from "react"
1+
import { useRef, useSyncExternalStore } from "react"
22
import { createLiveQueryCollection } from "@tanstack/db"
33
import type {
44
Collection,
@@ -55,58 +55,121 @@ export function useLiveQuery(
5555
typeof configOrQueryOrCollection.startSyncImmediate === `function` &&
5656
typeof configOrQueryOrCollection.id === `string`
5757

58-
const collection = useMemo(
59-
() => {
60-
if (isCollection) {
61-
// It's already a collection, ensure sync is started for React hooks
62-
configOrQueryOrCollection.startSyncImmediate()
63-
return configOrQueryOrCollection
64-
}
58+
// Use refs to cache collection and track dependencies
59+
const collectionRef = useRef<any>(null)
60+
const depsRef = useRef<Array<unknown> | null>(null)
61+
const configRef = useRef<any>(null)
62+
63+
// Check if we need to create/recreate the collection
64+
const needsNewCollection =
65+
!collectionRef.current ||
66+
(isCollection && configRef.current !== configOrQueryOrCollection) ||
67+
(!isCollection &&
68+
(depsRef.current === null ||
69+
depsRef.current.length !== deps.length ||
70+
depsRef.current.some((dep, i) => dep !== deps[i])))
6571

72+
if (needsNewCollection) {
73+
if (isCollection) {
74+
// It's already a collection, ensure sync is started for React hooks
75+
configOrQueryOrCollection.startSyncImmediate()
76+
collectionRef.current = configOrQueryOrCollection
77+
configRef.current = configOrQueryOrCollection
78+
} else {
6679
// Original logic for creating collections
6780
// Ensure we always start sync for React hooks
6881
if (typeof configOrQueryOrCollection === `function`) {
69-
return createLiveQueryCollection({
82+
collectionRef.current = createLiveQueryCollection({
7083
query: configOrQueryOrCollection,
7184
startSync: true,
85+
gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
7286
})
7387
} else {
74-
return createLiveQueryCollection({
75-
...configOrQueryOrCollection,
88+
collectionRef.current = createLiveQueryCollection({
7689
startSync: true,
90+
gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately
91+
...configOrQueryOrCollection,
7792
})
7893
}
79-
},
80-
isCollection ? [configOrQueryOrCollection] : [...deps]
81-
)
94+
depsRef.current = [...deps]
95+
}
96+
}
8297

83-
// Infer types from the actual collection
84-
type CollectionType =
85-
typeof collection extends Collection<infer T, any, any> ? T : never
86-
type KeyType =
87-
typeof collection extends Collection<any, infer K, any>
88-
? K
89-
: string | number
98+
// Use refs to track version and memoized snapshot
99+
const versionRef = useRef(0)
100+
const snapshotRef = useRef<{
101+
state: Map<any, any>
102+
data: Array<any>
103+
collection: Collection<any, any, any>
104+
_version: number
105+
} | null>(null)
90106

91-
// Use a simple counter to force re-renders when collection changes
92-
const [, forceUpdate] = useState(0)
107+
// Reset refs when collection changes
108+
if (needsNewCollection) {
109+
versionRef.current = 0
110+
snapshotRef.current = null
111+
}
93112

94-
useEffect(() => {
95-
// Subscribe to changes and force re-render
96-
const unsubscribe = collection.subscribeChanges(() => {
97-
forceUpdate((prev) => prev + 1)
98-
})
113+
// Create stable subscribe function using ref
114+
const subscribeRef = useRef<
115+
((onStoreChange: () => void) => () => void) | null
116+
>(null)
117+
if (!subscribeRef.current || needsNewCollection) {
118+
subscribeRef.current = (onStoreChange: () => void) => {
119+
const unsubscribe = collectionRef.current!.subscribeChanges(() => {
120+
versionRef.current += 1
121+
onStoreChange()
122+
})
123+
return () => {
124+
unsubscribe()
125+
}
126+
}
127+
}
128+
129+
// Create stable getSnapshot function using ref
130+
const getSnapshotRef = useRef<
131+
| (() => {
132+
state: Map<any, any>
133+
data: Array<any>
134+
collection: Collection<any, any, any>
135+
})
136+
| null
137+
>(null)
138+
if (!getSnapshotRef.current || needsNewCollection) {
139+
getSnapshotRef.current = () => {
140+
const currentVersion = versionRef.current
141+
const currentCollection = collectionRef.current!
142+
143+
// If we don't have a snapshot or the version changed, create a new one
144+
if (
145+
!snapshotRef.current ||
146+
snapshotRef.current._version !== currentVersion
147+
) {
148+
snapshotRef.current = {
149+
get state() {
150+
return new Map(currentCollection.entries())
151+
},
152+
get data() {
153+
return Array.from(currentCollection.values())
154+
},
155+
collection: currentCollection,
156+
_version: currentVersion,
157+
}
158+
}
99159

100-
return unsubscribe
101-
}, [collection])
160+
return snapshotRef.current
161+
}
162+
}
163+
164+
// Use useSyncExternalStore to subscribe to collection changes
165+
const snapshot = useSyncExternalStore(
166+
subscribeRef.current,
167+
getSnapshotRef.current
168+
)
102169

103170
return {
104-
get state(): Map<KeyType, CollectionType> {
105-
return new Map(collection.entries())
106-
},
107-
get data(): Array<CollectionType> {
108-
return Array.from(collection.values())
109-
},
110-
collection,
171+
state: snapshot.state,
172+
data: snapshot.data,
173+
collection: snapshot.collection,
111174
}
112175
}

0 commit comments

Comments
 (0)