Skip to content

Commit dadf96f

Browse files
authored
refactor(core): Use a Map internally for the QueryCache
* refactor(core): Use a Map internally instead of an Object + an Array * feat(core): Allow users to provide a custom cache creator function * test(core): Add tests for custom query cache * refactor(core): Use public constructor parameter * refactor(core): experimental_createStore - rename from createCache - expose store interface - pass in the cache into the createStore function (this is a bit weird because cache.queries doesn't exist yet, but it does once the store is created and returned :/ - add another test that checks if the "size limit" use-case works - mark as experimental because the interface might not be final * oops * refactor(core): expose cache from query so that we can access query.queryCache to find and remove other queries * docs: docs for custom store * types: export the QueryStore type * feat(custom-cache): revert exposing custom cache most things can be done by doing queryCache.subscribe, and we can still add this later if necessary * feat(custom-cache): cleanup forgotton export * stabilize a test
1 parent 24b0ddd commit dadf96f

File tree

6 files changed

+84
-51
lines changed

6 files changed

+84
-51
lines changed

docs/config.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,16 @@
177177
"to": "react/guides/does-this-replace-client-state"
178178
},
179179
{
180-
"label": "Migrating to React Query 3",
180+
"label": "Migrating to v3",
181181
"to": "react/guides/migrating-to-react-query-3"
182182
},
183183
{
184-
"label": "Migrating to React Query 4",
184+
"label": "Migrating to v4",
185185
"to": "react/guides/migrating-to-react-query-4"
186186
},
187187
{
188-
"label": "Migrating to React Query 5",
189-
"to": "react/guides/migrating-to-react-query-5"
188+
"label": "Migrating to v5",
189+
"to": "react/guides/migrating-v5"
190190
}
191191
]
192192
},

docs/react/guides/migrating-to-react-query-5.md renamed to docs/react/guides/migrating-to-v5.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
id: migrating-to-react-query-5
2+
id: migrating-to-v5
33
title: Migrating to TanStack Query v5
44
---
55

packages/query-core/src/query.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ export class Query<
159159
private observers: QueryObserver<any, any, any, any, any>[]
160160
private defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
161161
private abortSignalConsumed: boolean
162-
163162
constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
164163
super()
165164

@@ -174,11 +173,9 @@ export class Query<
174173
this.initialState = config.state || getDefaultState(this.options)
175174
this.state = this.initialState
176175
}
177-
178176
get meta(): QueryMeta | undefined {
179177
return this.options.meta
180178
}
181-
182179
private setOptions(
183180
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
184181
): void {

packages/query-core/src/queryCache.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ interface QueryCacheConfig {
1515
onSuccess?: (data: unknown, query: Query<unknown, unknown, unknown>) => void
1616
}
1717

18-
interface QueryHashMap {
19-
[hash: string]: Query<any, any, any, any>
20-
}
21-
2218
interface NotifyEventQueryAdded {
2319
type: 'added'
2420
query: Query<any, any, any, any>
@@ -72,16 +68,10 @@ type QueryCacheListener = (event: QueryCacheNotifyEvent) => void
7268
// CLASS
7369

7470
export class QueryCache extends Subscribable<QueryCacheListener> {
75-
config: QueryCacheConfig
76-
77-
private queries: Query<any, any, any, any>[]
78-
private queriesMap: QueryHashMap
71+
private queries = new Map<string, Query>()
7972

80-
constructor(config?: QueryCacheConfig) {
73+
constructor(public config: QueryCacheConfig = {}) {
8174
super()
82-
this.config = config || {}
83-
this.queries = []
84-
this.queriesMap = {}
8575
}
8676

8777
build<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
@@ -111,9 +101,9 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
111101
}
112102

113103
add(query: Query<any, any, any, any>): void {
114-
if (!this.queriesMap[query.queryHash]) {
115-
this.queriesMap[query.queryHash] = query
116-
this.queries.push(query)
104+
if (!this.queries.has(query.queryHash)) {
105+
this.queries.set(query.queryHash, query)
106+
117107
this.notify({
118108
type: 'added',
119109
query,
@@ -122,15 +112,13 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
122112
}
123113

124114
remove(query: Query<any, any, any, any>): void {
125-
const queryInMap = this.queriesMap[query.queryHash]
115+
const queryInMap = this.queries.get(query.queryHash)
126116

127117
if (queryInMap) {
128118
query.destroy()
129119

130-
this.queries = this.queries.filter((x) => x !== query)
131-
132120
if (queryInMap === query) {
133-
delete this.queriesMap[query.queryHash]
121+
this.queries.delete(query.queryHash)
134122
}
135123

136124
this.notify({ type: 'removed', query })
@@ -139,7 +127,7 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
139127

140128
clear(): void {
141129
notifyManager.batch(() => {
142-
this.queries.forEach((query) => {
130+
this.getAll().forEach((query) => {
143131
this.remove(query)
144132
})
145133
})
@@ -149,15 +137,17 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
149137
TQueryFnData = unknown,
150138
TError = Error,
151139
TData = TQueryFnData,
152-
TQueyKey extends QueryKey = QueryKey,
140+
TQueryKey extends QueryKey = QueryKey,
153141
>(
154142
queryHash: string,
155-
): Query<TQueryFnData, TError, TData, TQueyKey> | undefined {
156-
return this.queriesMap[queryHash]
143+
): Query<TQueryFnData, TError, TData, TQueryKey> | undefined {
144+
return this.queries.get(queryHash) as
145+
| Query<TQueryFnData, TError, TData, TQueryKey>
146+
| undefined
157147
}
158148

159149
getAll(): Query[] {
160-
return this.queries
150+
return [...this.queries.values()]
161151
}
162152

163153
find<TQueryFnData = unknown, TError = Error, TData = TQueryFnData>(
@@ -167,13 +157,16 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
167157
filters.exact = true
168158
}
169159

170-
return this.queries.find((query) => matchQuery(filters, query))
160+
return this.getAll().find((query) => matchQuery(filters, query)) as
161+
| Query<TQueryFnData, TError, TData>
162+
| undefined
171163
}
172164

173165
findAll(filters: QueryFilters = {}): Query[] {
166+
const queries = this.getAll()
174167
return Object.keys(filters).length > 0
175-
? this.queries.filter((query) => matchQuery(filters, query))
176-
: this.queries
168+
? queries.filter((query) => matchQuery(filters, query))
169+
: queries
177170
}
178171

179172
notify(event: QueryCacheNotifyEvent) {
@@ -186,15 +179,15 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
186179

187180
onFocus(): void {
188181
notifyManager.batch(() => {
189-
this.queries.forEach((query) => {
182+
this.getAll().forEach((query) => {
190183
query.onFocus()
191184
})
192185
})
193186
}
194187

195188
onOnline(): void {
196189
notifyManager.batch(() => {
197-
this.queries.forEach((query) => {
190+
this.getAll().forEach((query) => {
198191
query.onOnline()
199192
})
200193
})

packages/query-core/src/tests/queryCache.test.tsx

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { sleep, queryKey, createQueryClient } from './utils'
2-
import type { QueryClient } from '..'
2+
import { QueryClient } from '..'
33
import { QueryCache, QueryObserver } from '..'
44
import type { Query } from '.././query'
55
import { waitFor } from '@testing-library/react'
@@ -94,6 +94,46 @@ describe('queryCache', () => {
9494
await sleep(100)
9595
expect(callback).toHaveBeenCalled()
9696
})
97+
98+
test('should be able to limit cache size', async () => {
99+
const testCache = new QueryCache()
100+
101+
const unsubscribe = testCache.subscribe((event) => {
102+
if (event.type === 'added') {
103+
if (testCache.getAll().length > 2) {
104+
testCache
105+
.findAll({
106+
type: 'inactive',
107+
predicate: (q) => q !== event.query,
108+
})
109+
.forEach((query) => {
110+
testCache.remove(query)
111+
})
112+
}
113+
}
114+
})
115+
116+
const testClient = new QueryClient({ queryCache: testCache })
117+
118+
await testClient.prefetchQuery({
119+
queryKey: ['key1'],
120+
queryFn: () => 'data1',
121+
})
122+
expect(testCache.findAll().length).toBe(1)
123+
await testClient.prefetchQuery({
124+
queryKey: ['key2'],
125+
queryFn: () => 'data2',
126+
})
127+
expect(testCache.findAll().length).toBe(2)
128+
await testClient.prefetchQuery({
129+
queryKey: ['key3'],
130+
queryFn: () => 'data3',
131+
})
132+
expect(testCache.findAll().length).toBe(1)
133+
expect(testCache.findAll()[0]!.state.data).toBe('data3')
134+
135+
unsubscribe()
136+
})
97137
})
98138

99139
describe('find', () => {
@@ -286,15 +326,15 @@ describe('queryCache', () => {
286326

287327
// Directly add the query from the cache
288328
// to simulate a race condition
289-
const query = queryCache['queriesMap'][hash] as Query
329+
const query = queryCache['queries'].get(hash) as Query
290330
const queryClone = Object.assign({}, query)
291331

292332
// No error should be thrown when trying to add the query
293333
queryCache.add(queryClone)
294-
expect(queryCache['queries'].length).toEqual(1)
334+
expect(queryCache.getAll().length).toEqual(1)
295335

296336
// Clean-up to avoid an error when queryClient.clear()
297-
delete queryCache['queriesMap'][hash]
337+
queryCache['queries'].delete(hash)
298338
})
299339

300340
describe('QueryCache.remove', () => {
@@ -309,9 +349,9 @@ describe('queryCache', () => {
309349

310350
// Directly remove the query from the cache
311351
// to simulate a race condition
312-
const query = queryCache['queriesMap'][hash] as Query
352+
const query = queryCache['queries'].get(hash) as Query
313353
const queryClone = Object.assign({}, query)
314-
delete queryCache['queriesMap'][hash]
354+
queryCache['queries'].delete(hash)
315355

316356
// No error should be thrown when trying to remove the query
317357
expect(() => queryCache.remove(queryClone)).not.toThrow()

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -673,26 +673,29 @@ describe('useQuery', () => {
673673

674674
it('should call onSettled after a query has been fetched with an error', async () => {
675675
const key = queryKey()
676-
const states: UseQueryResult<string>[] = []
677676
const onSettled = jest.fn()
677+
const error = new Error('error')
678678

679679
function Page() {
680680
const state = useQuery({
681681
queryKey: key,
682-
queryFn: () => Promise.reject<unknown>('error'),
682+
queryFn: async () => {
683+
await sleep(10)
684+
return Promise.reject(error)
685+
},
683686
retry: false,
684687
onSettled,
685688
})
686-
states.push(state)
687-
return null
689+
return <div>status: {state.status}</div>
688690
}
689691

690-
renderWithClient(queryClient, <Page />)
692+
const rendered = renderWithClient(queryClient, <Page />)
691693

692-
await sleep(10)
693-
expect(states.length).toBe(2)
694+
await waitFor(() => {
695+
rendered.getByText('status: error')
696+
})
694697
expect(onSettled).toHaveBeenCalledTimes(1)
695-
expect(onSettled).toHaveBeenCalledWith(undefined, 'error')
698+
expect(onSettled).toHaveBeenCalledWith(undefined, error)
696699
})
697700

698701
it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => {

0 commit comments

Comments
 (0)