Skip to content
14 changes: 9 additions & 5 deletions src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ export class QueryCache {
0
)

this.globalListeners.forEach(d => d(queryCache, query))
this.globalListeners.forEach(d => d(this, query))
}

getDefaultConfig() {
return this.configRef.current
}

subscribe(listener: QueryCacheListener): () => void {
Expand Down Expand Up @@ -242,7 +246,7 @@ export class QueryCache {

if (!query) {
query = new Query<TResult, TError>({
queryCache,
queryCache: this,
queryKey,
queryHash,
config,
Expand Down Expand Up @@ -403,9 +407,9 @@ export class QueryCache {
}
}

export const queryCache = makeQueryCache({ frozen: isServer })

export const queryCaches = [queryCache]
const defaultQueryCache = makeQueryCache({ frozen: isServer })
export { defaultQueryCache as queryCache }
export const queryCaches = [defaultQueryCache]

export function makeQueryCache(config?: QueryCacheConfig) {
return new QueryCache(config)
Expand Down
178 changes: 111 additions & 67 deletions src/core/tests/queryCache.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sleep } from './utils'
import { queryCache, queryCaches } from '../'
import { queryCache as defaultQueryCache, queryCaches } from '../'
import { makeQueryCache } from '../queryCache'

describe('queryCache', () => {
Expand All @@ -10,25 +10,28 @@ describe('queryCache', () => {
test('setQueryData does not crash if query could not be found', () => {
const user = { userId: 1 }
expect(() =>
queryCache.setQueryData(['USER', user], (prevUser?: typeof user) => ({
...prevUser!,
name: 'Edvin',
}))
defaultQueryCache.setQueryData(
['USER', user],
(prevUser?: typeof user) => ({
...prevUser!,
name: 'Edvin',
})
)
).not.toThrow()
})

test('setQueryData does not crash when variable is null', () => {
queryCache.setQueryData(['USER', { userId: null }], 'Old Data')
defaultQueryCache.setQueryData(['USER', { userId: null }], 'Old Data')

expect(() =>
queryCache.setQueryData(['USER', { userId: null }], 'New Data')
defaultQueryCache.setQueryData(['USER', { userId: null }], 'New Data')
).not.toThrow()
})

// https://github.com/tannerlinsley/react-query/issues/652
test('prefetchQuery should not retry by default', async () => {
await expect(
queryCache.prefetchQuery(
defaultQueryCache.prefetchQuery(
'key',
async () => {
throw new Error('error')
Expand All @@ -41,16 +44,16 @@ describe('queryCache', () => {

test('prefetchQuery returns the cached data on cache hits', async () => {
const fetchFn = () => Promise.resolve('data')
const first = await queryCache.prefetchQuery('key', fetchFn)
const second = await queryCache.prefetchQuery('key', fetchFn)
const first = await defaultQueryCache.prefetchQuery('key', fetchFn)
const second = await defaultQueryCache.prefetchQuery('key', fetchFn)

expect(second).toBe(first)
})

test('prefetchQuery should not force fetch', async () => {
queryCache.setQueryData('key', 'og', { staleTime: 100 })
defaultQueryCache.setQueryData('key', 'og', { staleTime: 100 })
const fetchFn = () => Promise.resolve('new')
const first = await queryCache.prefetchQuery(
const first = await defaultQueryCache.prefetchQuery(
'key',
fetchFn,
{
Expand All @@ -65,7 +68,7 @@ describe('queryCache', () => {

test('prefetchQuery should throw error when throwOnError is true', async () => {
await expect(
queryCache.prefetchQuery(
defaultQueryCache.prefetchQuery(
'key',
async () => {
throw new Error('error')
Expand All @@ -81,9 +84,9 @@ describe('queryCache', () => {
test('should notify listeners when new query is added', async () => {
const callback = jest.fn()

queryCache.subscribe(callback)
defaultQueryCache.subscribe(callback)

queryCache.prefetchQuery('test', () => 'data')
defaultQueryCache.prefetchQuery('test', () => 'data')

await sleep(100)

Expand All @@ -93,91 +96,95 @@ describe('queryCache', () => {
test('should include the queryCache and query when notifying listeners', async () => {
const callback = jest.fn()

queryCache.subscribe(callback)
defaultQueryCache.subscribe(callback)

queryCache.prefetchQuery('test', () => 'data')
const query = queryCache.getQuery('test')
defaultQueryCache.prefetchQuery('test', () => 'data')
const query = defaultQueryCache.getQuery('test')

await sleep(100)

expect(callback).toHaveBeenCalledWith(queryCache, query)
expect(callback).toHaveBeenCalledWith(defaultQueryCache, query)
})

test('should notify subscribers when new query with initialData is added', async () => {
const callback = jest.fn()

queryCache.subscribe(callback)
defaultQueryCache.subscribe(callback)

queryCache.prefetchQuery('test', () => 'data', { initialData: 'initial' })
defaultQueryCache.prefetchQuery('test', () => 'data', {
initialData: 'initial',
})

await sleep(100)

expect(callback).toHaveBeenCalled()
})

test('setQueryData creates a new query if query was not found', () => {
queryCache.setQueryData('foo', 'bar')
defaultQueryCache.setQueryData('foo', 'bar')

expect(queryCache.getQueryData('foo')).toBe('bar')
expect(defaultQueryCache.getQueryData('foo')).toBe('bar')
})

test('setQueryData creates a new query if query was not found', () => {
queryCache.setQueryData('baz', 'qux')
defaultQueryCache.setQueryData('baz', 'qux')

expect(queryCache.getQueryData('baz')).toBe('qux')
expect(defaultQueryCache.getQueryData('baz')).toBe('qux')
})

test('removeQueries does not crash when exact is provided', async () => {
const fetchFn = () => Promise.resolve('data')

// check the query was added to the cache
await queryCache.prefetchQuery('key', fetchFn)
expect(queryCache.getQuery('key')).toBeTruthy()
await defaultQueryCache.prefetchQuery('key', fetchFn)
expect(defaultQueryCache.getQuery('key')).toBeTruthy()

// check the error doesn't occur
expect(() => queryCache.removeQueries('key', { exact: true })).not.toThrow()
expect(() =>
defaultQueryCache.removeQueries('key', { exact: true })
).not.toThrow()

// check query was successful removed
expect(queryCache.getQuery('key')).toBeFalsy()
expect(defaultQueryCache.getQuery('key')).toBeFalsy()
})

test('setQueryData should schedule stale timeout, if staleTime is set', async () => {
queryCache.setQueryData('key', 'test data', { staleTime: 10 })
defaultQueryCache.setQueryData('key', 'test data', { staleTime: 10 })
// @ts-expect-error
expect(queryCache.getQuery('key')!.staleTimeout).not.toBeUndefined()
expect(defaultQueryCache.getQuery('key')!.staleTimeout).not.toBeUndefined()
})

test('setQueryData should not schedule stale timeout by default', async () => {
queryCache.setQueryData('key', 'test data')
defaultQueryCache.setQueryData('key', 'test data')
// @ts-expect-error
expect(queryCache.getQuery('key')!.staleTimeout).toBeUndefined()
expect(defaultQueryCache.getQuery('key')!.staleTimeout).toBeUndefined()
})

test('setQueryData should not schedule stale timeout, if staleTime is set to `Infinity`', async () => {
queryCache.setQueryData('key', 'test data', { staleTime: Infinity })
defaultQueryCache.setQueryData('key', 'test data', { staleTime: Infinity })
// @ts-expect-error
expect(queryCache.getQuery('key')!.staleTimeout).toBeUndefined()
expect(defaultQueryCache.getQuery('key')!.staleTimeout).toBeUndefined()
})

test('setQueryData schedules stale timeouts appropriately', async () => {
queryCache.setQueryData('key', 'test data', { staleTime: 100 })
defaultQueryCache.setQueryData('key', 'test data', { staleTime: 100 })

expect(queryCache.getQuery('key')!.state.data).toEqual('test data')
expect(queryCache.getQuery('key')!.state.isStale).toEqual(false)
expect(defaultQueryCache.getQuery('key')!.state.data).toEqual('test data')
expect(defaultQueryCache.getQuery('key')!.state.isStale).toEqual(false)

await new Promise(resolve => setTimeout(resolve, 100))

expect(queryCache.getQuery('key')!.state.isStale).toEqual(true)
expect(defaultQueryCache.getQuery('key')!.state.isStale).toEqual(true)
})

test('setQueryData updater function works as expected', () => {
const updater = jest.fn(oldData => `new data + ${oldData}`)

queryCache.setQueryData('updater', 'test data')
queryCache.setQueryData('updater', updater)
defaultQueryCache.setQueryData('updater', 'test data')
defaultQueryCache.setQueryData('updater', updater)

expect(updater).toHaveBeenCalled()
expect(queryCache.getQuery('updater')!.state.data).toEqual(
expect(defaultQueryCache.getQuery('updater')!.state.data).toEqual(
'new data + test data'
)
})
Expand All @@ -186,33 +193,35 @@ describe('queryCache', () => {
const fetchData1 = () => Promise.resolve('data1')
const fetchData2 = () => Promise.resolve('data2')
const fetchDifferentData = () => Promise.resolve('data3')
await queryCache.prefetchQuery(['data', { page: 1 }], fetchData1)
await queryCache.prefetchQuery(['data', { page: 2 }], fetchData2)
await queryCache.prefetchQuery(['differentData'], fetchDifferentData)
const queries = queryCache.getQueries('data')
await defaultQueryCache.prefetchQuery(['data', { page: 1 }], fetchData1)
await defaultQueryCache.prefetchQuery(['data', { page: 2 }], fetchData2)
await defaultQueryCache.prefetchQuery(['differentData'], fetchDifferentData)
const queries = defaultQueryCache.getQueries('data')
const data = queries.map(query => query.state.data)
expect(data).toEqual(['data1', 'data2'])
})

test('stale timeout dispatch is not called if query is no longer in the query cache', async () => {
const queryKey = 'key'
const fetchData = () => Promise.resolve('data')
await queryCache.prefetchQuery(queryKey, fetchData, { staleTime: 100 })
const query = queryCache.getQuery(queryKey)
await defaultQueryCache.prefetchQuery(queryKey, fetchData, {
staleTime: 100,
})
const query = defaultQueryCache.getQuery(queryKey)
expect(query!.state.isStale).toBe(false)
queryCache.removeQueries(queryKey)
defaultQueryCache.removeQueries(queryKey)
await sleep(50)
expect(query!.state.isStale).toBe(false)
})

test('query interval is cleared when unsubscribed to a refetchInterval query', async () => {
const queryKey = 'key'
const fetchData = () => Promise.resolve('data')
await queryCache.prefetchQuery(queryKey, fetchData, {
await defaultQueryCache.prefetchQuery(queryKey, fetchData, {
cacheTime: 0,
refetchInterval: 1,
})
const query = queryCache.getQuery(queryKey)!
const query = defaultQueryCache.getQuery(queryKey)!
const instance = query.subscribe()
instance.updateConfig(query.config)
// @ts-expect-error
Expand All @@ -221,47 +230,82 @@ describe('queryCache', () => {
// @ts-expect-error
expect(instance.refetchIntervalId).toBeUndefined()
await sleep(10)
expect(queryCache.getQuery(queryKey)).toBeUndefined()
expect(defaultQueryCache.getQuery(queryKey)).toBeUndefined()
})

test('query is garbage collected when unsubscribed to', async () => {
const queryKey = 'key'
const fetchData = () => Promise.resolve('data')
await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
const query = queryCache.getQuery(queryKey)!
await defaultQueryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
const query = defaultQueryCache.getQuery(queryKey)!
expect(query.state.markedForGarbageCollection).toBe(false)
const instance = query.subscribe()
instance.unsubscribe()
expect(query.state.markedForGarbageCollection).toBe(true)
await sleep(10)
expect(queryCache.getQuery(queryKey)).toBeUndefined()
expect(defaultQueryCache.getQuery(queryKey)).toBeUndefined()
})

test('query is not garbage collected unless markedForGarbageCollection is true', async () => {
const queryKey = 'key'
const fetchData = () => Promise.resolve(undefined)
await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
const query = queryCache.getQuery(queryKey)!
await defaultQueryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
const query = defaultQueryCache.getQuery(queryKey)!
expect(query.state.markedForGarbageCollection).toBe(false)
const instance = query.subscribe()
instance.unsubscribe()
expect(query.state.markedForGarbageCollection).toBe(true)
queryCache.clear({ notify: false })
queryCache.setQueryData(queryKey, 'data')
defaultQueryCache.clear({ notify: false })
defaultQueryCache.setQueryData(queryKey, 'data')
await sleep(10)
const newQuery = queryCache.getQuery(queryKey)!
const newQuery = defaultQueryCache.getQuery(queryKey)!
expect(newQuery.state.markedForGarbageCollection).toBe(false)
expect(newQuery.state.data).toBe('data')
})

test('makeQueryCache merges defaultConfig so providing a queryFn does not overwrite the default queryKeySerializerFn', async () => {
const queryFn = () => 'data'
const queryCache = makeQueryCache({
defaultConfig: { queries: { queryFn } },
describe('makeQueryCache', () => {
test('merges defaultConfig so providing a queryFn does not overwrite the default queryKeySerializerFn', async () => {
const queryFn = () => 'data'
const queryCache = makeQueryCache({
defaultConfig: { queries: { queryFn } },
})

expect(() => queryCache.buildQuery('test')).not.toThrow(
'config.queryKeySerializerFn is not a function'
)
})

expect(() => queryCache.buildQuery('test')).not.toThrow(
'config.queryKeySerializerFn is not a function'
)
test('merges defaultConfig when query is added to cache', async () => {
const queryCache = makeQueryCache({
defaultConfig: {
queries: { refetchOnMount: false, staleTime: Infinity },
},
})

const queryKey = 'key'
const fetchData = () => Promise.resolve(undefined)
await queryCache.prefetchQuery(queryKey, fetchData)
const newQuery = queryCache.getQuery(queryKey)
expect(newQuery?.config.staleTime).toBe(Infinity)
expect(newQuery?.config.refetchOnMount).toBe(false)
})

test('built queries are referencing the correct queryCache', () => {
const queryCache = makeQueryCache()
const query = queryCache.buildQuery('test')

expect(query.queryCache).toBe(queryCache)
})

test('notifyGlobalListeners passes the same instance', () => {
const queryCache = makeQueryCache()
const subscriber = jest.fn()
const unsubscribe = queryCache.subscribe(subscriber)
const query = queryCache.buildQuery('test')
query.setData('foo');
expect(subscriber).toHaveBeenCalledWith(queryCache, query)

unsubscribe()
})
})
})
Loading