Skip to content

Commit 9f34ce8

Browse files
committed
feat: add refetchType option for more granular refetching control
1 parent 5bd4525 commit 9f34ce8

File tree

3 files changed

+246
-2
lines changed

3 files changed

+246
-2
lines changed

docs/collections/query-collection.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ The `queryCollectionOptions` function accepts the following options:
5757

5858
- `select`: Function that lets extract array items when they’re wrapped with metadata
5959
- `enabled`: Whether the query should automatically run (default: `true`)
60+
- `refetchType`: The type of refetch to perform (default: `all`)
61+
- `all`: Refetch this collection regardless of observer state
62+
- `active`: Refetch only when there is an active observer
63+
- `inactive`: Refetch only when there is no active observer
64+
- Notes:
65+
- Refetch only targets queries that already exist in the TanStack Query cache for the exact `queryKey`
66+
- If `enabled: false`, `utils.refetch()` is a no-op for all `refetchType` values
67+
- An "active observer" exists while the collection is syncing (e.g. when `startSync: true` or once started manually)
6068
- `refetchInterval`: Refetch interval in milliseconds
6169
- `retry`: Retry configuration for failed queries
6270
- `retryDelay`: Delay between retries
@@ -135,7 +143,9 @@ This is useful when:
135143

136144
The collection provides these utility methods via `collection.utils`:
137145

138-
- `refetch()`: Manually trigger a refetch of the query
146+
- `refetch(opts?)`: Manually trigger a refetch of the query
147+
- `opts.throwOnError`: Whether to throw an error if the refetch fails (default: `false`)
148+
- Targets only the exact `queryKey` and respects `refetchType` (`'all' | 'active' | 'inactive'`).
139149

140150
## Direct Writes
141151

@@ -348,4 +358,4 @@ All direct write methods are available on `collection.utils`:
348358
- `writeDelete(keys)`: Delete one or more items directly
349359
- `writeUpsert(data)`: Insert or update one or more items directly
350360
- `writeBatch(callback)`: Perform multiple operations atomically
351-
- `refetch()`: Manually trigger a refetch of the query
361+
- `refetch(opts?)`: Manually trigger a refetch of the query

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ export interface QueryCollectionConfig<
7777
// Query-specific options
7878
/** Whether the query should automatically run (default: true) */
7979
enabled?: boolean
80+
/**
81+
* The type of refetch to perform (default: all)
82+
* - `all`: Refetch this collection regardless of observer state
83+
* - `active`: Refetch only when there is an active observer
84+
* - `inactive`: Refetch only when there is no active observer
85+
*
86+
* Notes:
87+
* - Refetch only targets queries that already exist in the TanStack Query cache for the exact `queryKey`
88+
* - If `enabled: false`, `utils.refetch()` is a no-op for all `refetchType` values
89+
* - An "active observer" exists while the collection is syncing (e.g. when `startSync: true` or once started manually)
90+
*/
91+
refetchType?: `active` | `inactive` | `all`
8092
refetchInterval?: QueryObserverOptions<
8193
Array<T>,
8294
TError,
@@ -381,6 +393,7 @@ export function queryCollectionOptions(
381393
select,
382394
queryClient,
383395
enabled,
396+
refetchType = `all`,
384397
refetchInterval,
385398
retry,
386399
retryDelay,
@@ -602,6 +615,7 @@ export function queryCollectionOptions(
602615
{
603616
queryKey: queryKey,
604617
exact: true,
618+
type: refetchType,
605619
},
606620
{
607621
throwOnError: opts?.throwOnError,

packages/query-db-collection/tests/query.test.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,226 @@ describe(`QueryCollection`, () => {
20332033
])
20342034
})
20352035

2036+
describe(`refetchType`, () => {
2037+
it(`should refetch for 'all' when no observers exist`, async () => {
2038+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
2039+
const queryKey = [`refetch-all-test-query`]
2040+
const queryFn = vi.fn().mockResolvedValue(mockItems)
2041+
2042+
// TanStack Query only refetches queries that already exist in the cache
2043+
await queryClient.prefetchQuery({ queryKey, queryFn })
2044+
expect(queryFn).toHaveBeenCalledTimes(1)
2045+
2046+
const collection = createCollection(
2047+
queryCollectionOptions({
2048+
id: `refetch-all-test-query`,
2049+
queryClient,
2050+
queryKey,
2051+
queryFn,
2052+
getKey,
2053+
refetchType: `all`,
2054+
// Do not start sync: no observers -> inactive
2055+
startSync: false,
2056+
})
2057+
)
2058+
2059+
// Clear mock to test refetch behavior
2060+
queryFn.mockClear()
2061+
2062+
await collection.utils.refetch()
2063+
expect(queryFn).toHaveBeenCalledTimes(1)
2064+
})
2065+
2066+
it(`should refetch for 'all' when an active observer exists`, async () => {
2067+
const queryKey = [`refetch-all-test-query`]
2068+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
2069+
2070+
// TanStack Query only refetches queries that already exist in the cache
2071+
await queryClient.prefetchQuery({ queryKey, queryFn })
2072+
2073+
const collection = createCollection(
2074+
queryCollectionOptions({
2075+
id: `refetch-all-test-query`,
2076+
queryClient,
2077+
queryKey,
2078+
queryFn,
2079+
getKey,
2080+
refetchType: `all`,
2081+
startSync: true,
2082+
})
2083+
)
2084+
2085+
// Clear mock to test refetch behavior
2086+
queryFn.mockClear()
2087+
2088+
await collection.utils.refetch()
2089+
expect(queryFn).toHaveBeenCalledTimes(1)
2090+
})
2091+
2092+
it(`should be no-op for 'active' when no observers exist`, async () => {
2093+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
2094+
const queryKey = [`refetch-active-test-query`]
2095+
const queryFn = vi.fn().mockResolvedValue(mockItems)
2096+
2097+
// TanStack Query only refetches queries that already exist in the cache
2098+
await queryClient.prefetchQuery({ queryKey, queryFn })
2099+
expect(queryFn).toHaveBeenCalledTimes(1)
2100+
2101+
const collection = createCollection(
2102+
queryCollectionOptions({
2103+
id: `refetch-active-test-query`,
2104+
queryClient,
2105+
queryKey,
2106+
queryFn,
2107+
getKey,
2108+
refetchType: `active`,
2109+
// Do not start sync: no observers -> inactive
2110+
startSync: false,
2111+
})
2112+
)
2113+
2114+
// Clear mock to test refetch behavior
2115+
queryFn.mockClear()
2116+
2117+
await collection.utils.refetch()
2118+
expect(queryFn).not.toHaveBeenCalled()
2119+
})
2120+
2121+
it(`should refetch for 'active' when an active observer exists`, async () => {
2122+
const queryKey = [`refetch-active-test-query`]
2123+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
2124+
2125+
// TanStack Query only refetches queries that already exist in the cache
2126+
await queryClient.prefetchQuery({ queryKey, queryFn })
2127+
2128+
const collection = createCollection(
2129+
queryCollectionOptions({
2130+
id: `refetch-active-test-query`,
2131+
queryClient,
2132+
queryKey,
2133+
queryFn,
2134+
getKey,
2135+
refetchType: `active`,
2136+
startSync: true, // observer exists but query is disabled
2137+
})
2138+
)
2139+
2140+
// Clear mock to test refetch behavior
2141+
queryFn.mockClear()
2142+
2143+
await collection.utils.refetch()
2144+
expect(queryFn).toHaveBeenCalledTimes(1)
2145+
})
2146+
2147+
it(`should refetch for 'inactive' when no observers exist`, async () => {
2148+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
2149+
const queryKey = [`refetch-inactive-test-query`]
2150+
const queryFn = vi.fn().mockResolvedValue(mockItems)
2151+
2152+
// TanStack Query only refetches queries that already exist in the cache
2153+
await queryClient.prefetchQuery({ queryKey, queryFn })
2154+
expect(queryFn).toHaveBeenCalledTimes(1)
2155+
2156+
const collection = createCollection(
2157+
queryCollectionOptions({
2158+
id: `refetch-inactive-test-query`,
2159+
queryClient,
2160+
queryKey,
2161+
queryFn,
2162+
getKey,
2163+
refetchType: `inactive`,
2164+
// Do not start sync: no observers -> inactive
2165+
startSync: false,
2166+
})
2167+
)
2168+
2169+
// Clear mock to test refetch behavior
2170+
queryFn.mockClear()
2171+
2172+
await collection.utils.refetch()
2173+
expect(queryFn).toHaveBeenCalledTimes(1)
2174+
})
2175+
2176+
it(`should be no-op for 'inactive' when an active observer exists`, async () => {
2177+
const queryKey = [`refetch-inactive-test-query`]
2178+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
2179+
2180+
// TanStack Query only refetches queries that already exist in the cache
2181+
await queryClient.prefetchQuery({ queryKey, queryFn })
2182+
2183+
const collection = createCollection(
2184+
queryCollectionOptions({
2185+
id: `refetch-inactive-test-query`,
2186+
queryClient,
2187+
queryKey,
2188+
queryFn,
2189+
getKey,
2190+
refetchType: `inactive`,
2191+
startSync: true,
2192+
})
2193+
)
2194+
2195+
// Clear mock to test refetch behavior
2196+
queryFn.mockClear()
2197+
2198+
await collection.utils.refetch()
2199+
expect(queryFn).not.toHaveBeenCalled()
2200+
})
2201+
2202+
it(`should be no-op for all refetchType values when query is not in cache`, async () => {
2203+
const base = `no-cache-refetch-test-query`
2204+
for (const type of [`active`, `inactive`, `all`] as const) {
2205+
const queryKey = [base, type]
2206+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
2207+
2208+
const collection = createCollection(
2209+
queryCollectionOptions({
2210+
id: `no-cache-refetch-test-query-${type}`,
2211+
queryClient,
2212+
queryKey,
2213+
queryFn,
2214+
getKey,
2215+
refetchType: type,
2216+
startSync: false, // no observer; also do not prefetch
2217+
})
2218+
)
2219+
2220+
await collection.utils.refetch()
2221+
expect(queryFn).not.toHaveBeenCalled()
2222+
}
2223+
})
2224+
2225+
it(`should be no-op for all refetchType values when query is disabled`, async () => {
2226+
const base = `refetch-test-query`
2227+
for (const type of [`active`, `inactive`, `all`] as const) {
2228+
const queryKey = [base, type]
2229+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
2230+
2231+
// TanStack Query only refetches queries that already exist in the cache
2232+
await queryClient.prefetchQuery({ queryKey, queryFn })
2233+
2234+
const collection = createCollection(
2235+
queryCollectionOptions({
2236+
id: `no-cache-refetch-test-query-${type}`,
2237+
queryClient,
2238+
queryKey,
2239+
queryFn,
2240+
getKey,
2241+
refetchType: type,
2242+
startSync: true,
2243+
enabled: false,
2244+
})
2245+
)
2246+
2247+
// Clear mock to test refetch behavior
2248+
queryFn.mockClear()
2249+
2250+
await collection.utils.refetch()
2251+
expect(queryFn).not.toHaveBeenCalled()
2252+
}
2253+
})
2254+
})
2255+
20362256
describe(`Error Handling`, () => {
20372257
// Helper to create test collection with common configuration
20382258
const createErrorHandlingTestCollection = (

0 commit comments

Comments
 (0)