Skip to content

Commit eabfbea

Browse files
committed
feat: add refetchType option for more granular refetching control
1 parent 3d83ab3 commit eabfbea

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
@@ -56,6 +56,14 @@ The `queryCollectionOptions` function accepts the following options:
5656
### Query Options
5757

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

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

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

139149
## Direct Writes
140150

@@ -347,4 +357,4 @@ All direct write methods are available on `collection.utils`:
347357
- `writeDelete(keys)`: Delete one or more items directly
348358
- `writeUpsert(data)`: Insert or update one or more items directly
349359
- `writeBatch(callback)`: Perform multiple operations atomically
350-
- `refetch()`: Manually trigger a refetch of the query
360+
- `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
@@ -92,6 +92,18 @@ export interface QueryCollectionConfig<
9292
// Query-specific options
9393
/** Whether the query should automatically run (default: true) */
9494
enabled?: boolean
95+
/**
96+
* The type of refetch to perform (default: all)
97+
* - `all`: Refetch this collection regardless of observer state
98+
* - `active`: Refetch only when there is an active observer
99+
* - `inactive`: Refetch only when there is no active observer
100+
*
101+
* Notes:
102+
* - Refetch only targets queries that already exist in the TanStack Query cache for the exact `queryKey`
103+
* - If `enabled: false`, `utils.refetch()` is a no-op for all `refetchType` values
104+
* - An "active observer" exists while the collection is syncing (e.g. when `startSync: true` or once started manually)
105+
*/
106+
refetchType?: `active` | `inactive` | `all`
95107
refetchInterval?: QueryObserverOptions<
96108
Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
97109
TError,
@@ -452,6 +464,7 @@ export function queryCollectionOptions<
452464
queryFn,
453465
queryClient,
454466
enabled,
467+
refetchType = `all`,
455468
refetchInterval,
456469
retry,
457470
retryDelay,
@@ -635,6 +648,7 @@ export function queryCollectionOptions<
635648
{
636649
queryKey: queryKey,
637650
exact: true,
651+
type: refetchType,
638652
},
639653
{
640654
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
@@ -1708,6 +1708,226 @@ describe(`QueryCollection`, () => {
17081708
])
17091709
})
17101710

1711+
describe(`refetchType`, () => {
1712+
it(`should refetch for 'all' when no observers exist`, async () => {
1713+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
1714+
const queryKey = [`refetch-all-test-query`]
1715+
const queryFn = vi.fn().mockResolvedValue(mockItems)
1716+
1717+
// TanStack Query only refetches queries that already exist in the cache
1718+
await queryClient.prefetchQuery({ queryKey, queryFn })
1719+
expect(queryFn).toHaveBeenCalledTimes(1)
1720+
1721+
const collection = createCollection(
1722+
queryCollectionOptions({
1723+
id: `refetch-all-test-query`,
1724+
queryClient,
1725+
queryKey,
1726+
queryFn,
1727+
getKey,
1728+
refetchType: `all`,
1729+
// Do not start sync: no observers -> inactive
1730+
startSync: false,
1731+
})
1732+
)
1733+
1734+
// Clear mock to test refetch behavior
1735+
queryFn.mockClear()
1736+
1737+
await collection.utils.refetch()
1738+
expect(queryFn).toHaveBeenCalledTimes(1)
1739+
})
1740+
1741+
it(`should refetch for 'all' when an active observer exists`, async () => {
1742+
const queryKey = [`refetch-all-test-query`]
1743+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
1744+
1745+
// TanStack Query only refetches queries that already exist in the cache
1746+
await queryClient.prefetchQuery({ queryKey, queryFn })
1747+
1748+
const collection = createCollection(
1749+
queryCollectionOptions({
1750+
id: `refetch-all-test-query`,
1751+
queryClient,
1752+
queryKey,
1753+
queryFn,
1754+
getKey,
1755+
refetchType: `all`,
1756+
startSync: true,
1757+
})
1758+
)
1759+
1760+
// Clear mock to test refetch behavior
1761+
queryFn.mockClear()
1762+
1763+
await collection.utils.refetch()
1764+
expect(queryFn).toHaveBeenCalledTimes(1)
1765+
})
1766+
1767+
it(`should be no-op for 'active' when no observers exist`, async () => {
1768+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
1769+
const queryKey = [`refetch-active-test-query`]
1770+
const queryFn = vi.fn().mockResolvedValue(mockItems)
1771+
1772+
// TanStack Query only refetches queries that already exist in the cache
1773+
await queryClient.prefetchQuery({ queryKey, queryFn })
1774+
expect(queryFn).toHaveBeenCalledTimes(1)
1775+
1776+
const collection = createCollection(
1777+
queryCollectionOptions({
1778+
id: `refetch-active-test-query`,
1779+
queryClient,
1780+
queryKey,
1781+
queryFn,
1782+
getKey,
1783+
refetchType: `active`,
1784+
// Do not start sync: no observers -> inactive
1785+
startSync: false,
1786+
})
1787+
)
1788+
1789+
// Clear mock to test refetch behavior
1790+
queryFn.mockClear()
1791+
1792+
await collection.utils.refetch()
1793+
expect(queryFn).not.toHaveBeenCalled()
1794+
})
1795+
1796+
it(`should refetch for 'active' when an active observer exists`, async () => {
1797+
const queryKey = [`refetch-active-test-query`]
1798+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
1799+
1800+
// TanStack Query only refetches queries that already exist in the cache
1801+
await queryClient.prefetchQuery({ queryKey, queryFn })
1802+
1803+
const collection = createCollection(
1804+
queryCollectionOptions({
1805+
id: `refetch-active-test-query`,
1806+
queryClient,
1807+
queryKey,
1808+
queryFn,
1809+
getKey,
1810+
refetchType: `active`,
1811+
startSync: true, // observer exists but query is disabled
1812+
})
1813+
)
1814+
1815+
// Clear mock to test refetch behavior
1816+
queryFn.mockClear()
1817+
1818+
await collection.utils.refetch()
1819+
expect(queryFn).toHaveBeenCalledTimes(1)
1820+
})
1821+
1822+
it(`should refetch for 'inactive' when no observers exist`, async () => {
1823+
const mockItems: Array<TestItem> = [{ id: `1`, name: `Item 1` }]
1824+
const queryKey = [`refetch-inactive-test-query`]
1825+
const queryFn = vi.fn().mockResolvedValue(mockItems)
1826+
1827+
// TanStack Query only refetches queries that already exist in the cache
1828+
await queryClient.prefetchQuery({ queryKey, queryFn })
1829+
expect(queryFn).toHaveBeenCalledTimes(1)
1830+
1831+
const collection = createCollection(
1832+
queryCollectionOptions({
1833+
id: `refetch-inactive-test-query`,
1834+
queryClient,
1835+
queryKey,
1836+
queryFn,
1837+
getKey,
1838+
refetchType: `inactive`,
1839+
// Do not start sync: no observers -> inactive
1840+
startSync: false,
1841+
})
1842+
)
1843+
1844+
// Clear mock to test refetch behavior
1845+
queryFn.mockClear()
1846+
1847+
await collection.utils.refetch()
1848+
expect(queryFn).toHaveBeenCalledTimes(1)
1849+
})
1850+
1851+
it(`should be no-op for 'inactive' when an active observer exists`, async () => {
1852+
const queryKey = [`refetch-inactive-test-query`]
1853+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
1854+
1855+
// TanStack Query only refetches queries that already exist in the cache
1856+
await queryClient.prefetchQuery({ queryKey, queryFn })
1857+
1858+
const collection = createCollection(
1859+
queryCollectionOptions({
1860+
id: `refetch-inactive-test-query`,
1861+
queryClient,
1862+
queryKey,
1863+
queryFn,
1864+
getKey,
1865+
refetchType: `inactive`,
1866+
startSync: true,
1867+
})
1868+
)
1869+
1870+
// Clear mock to test refetch behavior
1871+
queryFn.mockClear()
1872+
1873+
await collection.utils.refetch()
1874+
expect(queryFn).not.toHaveBeenCalled()
1875+
})
1876+
1877+
it(`should be no-op for all refetchType values when query is not in cache`, async () => {
1878+
const base = `no-cache-refetch-test-query`
1879+
for (const type of [`active`, `inactive`, `all`] as const) {
1880+
const queryKey = [base, type]
1881+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
1882+
1883+
const collection = createCollection(
1884+
queryCollectionOptions({
1885+
id: `no-cache-refetch-test-query-${type}`,
1886+
queryClient,
1887+
queryKey,
1888+
queryFn,
1889+
getKey,
1890+
refetchType: type,
1891+
startSync: false, // no observer; also do not prefetch
1892+
})
1893+
)
1894+
1895+
await collection.utils.refetch()
1896+
expect(queryFn).not.toHaveBeenCalled()
1897+
}
1898+
})
1899+
1900+
it(`should be no-op for all refetchType values when query is disabled`, async () => {
1901+
const base = `refetch-test-query`
1902+
for (const type of [`active`, `inactive`, `all`] as const) {
1903+
const queryKey = [base, type]
1904+
const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `A` }])
1905+
1906+
// TanStack Query only refetches queries that already exist in the cache
1907+
await queryClient.prefetchQuery({ queryKey, queryFn })
1908+
1909+
const collection = createCollection(
1910+
queryCollectionOptions({
1911+
id: `no-cache-refetch-test-query-${type}`,
1912+
queryClient,
1913+
queryKey,
1914+
queryFn,
1915+
getKey,
1916+
refetchType: type,
1917+
startSync: true,
1918+
enabled: false,
1919+
})
1920+
)
1921+
1922+
// Clear mock to test refetch behavior
1923+
queryFn.mockClear()
1924+
1925+
await collection.utils.refetch()
1926+
expect(queryFn).not.toHaveBeenCalled()
1927+
}
1928+
})
1929+
})
1930+
17111931
describe(`Error Handling`, () => {
17121932
// Helper to create test collection with common configuration
17131933
const createErrorHandlingTestCollection = (

0 commit comments

Comments
 (0)