|
1 |
| -import { useEffect, useMemo, useState } from "react" |
| 1 | +import { useRef, useSyncExternalStore } from "react" |
2 | 2 | import { createLiveQueryCollection } from "@tanstack/db"
|
3 | 3 | import type {
|
4 | 4 | Collection,
|
@@ -55,58 +55,121 @@ export function useLiveQuery(
|
55 | 55 | typeof configOrQueryOrCollection.startSyncImmediate === `function` &&
|
56 | 56 | typeof configOrQueryOrCollection.id === `string`
|
57 | 57 |
|
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]))) |
65 | 71 |
|
| 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 { |
66 | 79 | // Original logic for creating collections
|
67 | 80 | // Ensure we always start sync for React hooks
|
68 | 81 | if (typeof configOrQueryOrCollection === `function`) {
|
69 |
| - return createLiveQueryCollection({ |
| 82 | + collectionRef.current = createLiveQueryCollection({ |
70 | 83 | query: configOrQueryOrCollection,
|
71 | 84 | startSync: true,
|
| 85 | + gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately |
72 | 86 | })
|
73 | 87 | } else {
|
74 |
| - return createLiveQueryCollection({ |
75 |
| - ...configOrQueryOrCollection, |
| 88 | + collectionRef.current = createLiveQueryCollection({ |
76 | 89 | startSync: true,
|
| 90 | + gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately |
| 91 | + ...configOrQueryOrCollection, |
77 | 92 | })
|
78 | 93 | }
|
79 |
| - }, |
80 |
| - isCollection ? [configOrQueryOrCollection] : [...deps] |
81 |
| - ) |
| 94 | + depsRef.current = [...deps] |
| 95 | + } |
| 96 | + } |
82 | 97 |
|
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) |
90 | 106 |
|
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 | + } |
93 | 112 |
|
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 | + } |
99 | 159 |
|
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 | + ) |
102 | 169 |
|
103 | 170 | 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, |
111 | 174 | }
|
112 | 175 | }
|
0 commit comments