Skip to content

Commit

Permalink
feat: query cache provider
Browse files Browse the repository at this point in the history
the query cache now comes from a context, meaning users can easily override and control the cache being used inside their application
  • Loading branch information
Jack Ellis committed May 11, 2020
1 parent ff5d6a7 commit 66777c6
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 76 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,10 @@ This library is being built and maintained by me, @tannerlinsley and I am always
- [`queryCache.isFetching`](#querycacheisfetching)
- [`queryCache.subscribe`](#querycachesubscribe)
- [`queryCache.clear`](#querycacheclear)
- [`useQueryCache`](#usequerycache)
- [`useIsFetching`](#useisfetching)
- [`ReactQueryConfigProvider`](#reactqueryconfigprovider)
- [`ReactQueryCacheProvider`](#reactquerycacheprovider)
- [`setConsole`](#setconsole)
- [Contributors ✨](#contributors-)

Expand Down Expand Up @@ -2548,6 +2550,18 @@ queryCache.clear()
- `queries: Array<Query>`
- This will be an array containing the queries that were found.
## `useQueryCache`
The `useQueryCache` hook returns the current queryCache instance.
```js
import { useQueryCache } from 'react-query';

const queryCache = useQueryCache()
```
If you are using the `ReactQueryCacheProvider` to set a custom cache, you cannot simply import `{ queryCache }` any more. This hook will ensure you're getting the correct instance.
## `useIsFetching`
`useIsFetching` is an optional hook that returns the `number` of the queries that your application is loading or fetching in the background (useful for app-wide loading indicators).
Expand Down Expand Up @@ -2608,6 +2622,29 @@ function App() {
- Must be **stable** or **memoized**. Do not create an inline object!
- For non-global properties please see their usage in both the [`useQuery` hook](#usequery) and the [`useMutation` hook](#usemutation).
## `ReactQueryCacheProvider`
`ReactQueryCacheProvider` is an optional provider component for explicitly setting the query cache used by `useQuery`. This is useful for creating component-level caches that are not completely global, as well as making truly isolated unit tests.
```js
import { ReactQueryCacheProvider, makeQueryCache } from 'react-query';

const queryCache = makeQueryCache()

function App() {
return (
<ReactQueryCacheProvider queryCache={queryCache}>
...
</ReactQueryCacheProvider>
)
}
```
### Options
- `queryCache: Object`
- In instance of queryCache, you can use the `makeQueryCache` factory to create this.
- If not provided, a new cache will be generated.
## `setConsole`
`setConsole` is an optional utility function that allows you to replace the `console` interface used to log errors. By default, the `window.console` object is used. If no global `console` object is found in the environment, nothing will be logged.
Expand Down
7 changes: 6 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { queryCache } from './queryCache'
export {
queryCache,
makeQueryCache,
ReactQueryCacheProvider,
useQueryCache,
} from './queryCache'
export { ReactQueryConfigProvider } from './config'
export { setFocusHandler } from './setFocusHandler'
export { useIsFetching } from './useIsFetching'
Expand Down
7 changes: 6 additions & 1 deletion src/index.production.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { queryCache } from './queryCache'
export {
queryCache,
makeQueryCache,
ReactQueryCacheProvider,
useQueryCache,
} from './queryCache'
export { ReactQueryConfigProvider } from './config'
export { setFocusHandler } from './setFocusHandler'
export { useIsFetching } from './useIsFetching'
Expand Down
24 changes: 23 additions & 1 deletion src/queryCache.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import {
isServer,
functionalUpdate,
Expand All @@ -14,6 +15,25 @@ import { defaultConfigRef } from './config'

export const queryCache = makeQueryCache()

export const defaultQueryCacheRef = {
current: queryCache
}

export const queryCacheContext = React.createContext(queryCache)

export function useQueryCache() {
return React.useContext(queryCacheContext)
}

export function ReactQueryCacheProvider({ queryCache, children }) {
defaultQueryCacheRef.current = queryCache || makeQueryCache()
return (
<queryCacheContext.Provider value={defaultQueryCacheRef.current}>
{children}
</queryCacheContext.Provider>
)
}

const actionInit = {}
const actionFailed = {}
const actionMarkStale = {}
Expand All @@ -32,7 +52,7 @@ export function makeQueryCache() {
}

const notifyGlobalListeners = () => {
cache.isFetching = Object.values(queryCache.queries).reduce(
cache.isFetching = Object.values(cache.queries).reduce(
(acc, query) => (query.state.isFetching ? acc + 1 : acc),
0
)
Expand Down Expand Up @@ -113,6 +133,7 @@ export function makeQueryCache() {
query.config = { ...query.config, ...config }
} else {
query = makeQuery({
cache,
queryKey,
queryHash,
queryVariables,
Expand Down Expand Up @@ -196,6 +217,7 @@ export function makeQueryCache() {
}

function makeQuery(options) {
const queryCache = options.cache
const reducer = options.config.queryReducer || defaultQueryReducer

const noQueryHash = typeof options.queryHash === 'undefined'
Expand Down
4 changes: 2 additions & 2 deletions src/setFocusHandler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isOnline, isDocumentVisible, Console, isServer } from './utils'
import { defaultConfigRef } from './config'
import { queryCache } from './queryCache'
import { defaultQueryCacheRef } from './queryCache'

const visibilityChangeEvent = 'visibilitychange'
const focusEvent = 'focus'
Expand All @@ -9,7 +9,7 @@ const onWindowFocus = () => {
const { refetchAllOnWindowFocus } = defaultConfigRef.current

if (isDocumentVisible() && isOnline()) {
queryCache
defaultQueryCacheRef.current
.refetchQueries(query => {
if (!query.instances.length) {
return false
Expand Down
7 changes: 4 additions & 3 deletions src/tests/config.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react'
import { render, waitForElement, cleanup } from '@testing-library/react'
import { ReactQueryConfigProvider, useQuery, queryCache } from '../index'
import { ReactQueryConfigProvider, useQuery, ReactQueryCacheProvider } from '../index'

import { sleep } from './utils'

describe('config', () => {
afterEach(() => {
queryCache.clear()
cleanup()
})

Expand All @@ -33,7 +32,9 @@ describe('config', () => {

const rendered = render(
<ReactQueryConfigProvider config={config}>
<Page />
<ReactQueryCacheProvider>
<Page />
</ReactQueryCacheProvider>
</ReactQueryConfigProvider>
)

Expand Down
22 changes: 17 additions & 5 deletions src/tests/queryCache.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { queryCache } from '../index'
import { act } from '@testing-library/react'
import { makeQueryCache } from '../index'
import { sleep } from './utils'

describe('queryCache', () => {
afterEach(() => {
queryCache.clear()
})

test('setQueryData does not crash if query could not be found', () => {
const queryCache = makeQueryCache();
expect(() =>
queryCache.setQueryData(['USER', { userId: 1 }], prevUser => ({
...prevUser,
Expand All @@ -17,6 +14,7 @@ describe('queryCache', () => {
})

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

expect(() =>
Expand All @@ -25,6 +23,7 @@ describe('queryCache', () => {
})

test('prefetchQuery returns the cached data on cache hits', async () => {
const queryCache = makeQueryCache();
const fetchFn = () => Promise.resolve('data')
const first = await queryCache.prefetchQuery('key', fetchFn)
const second = await queryCache.prefetchQuery('key', fetchFn)
Expand All @@ -33,6 +32,7 @@ describe('queryCache', () => {
})

test('prefetchQuery should force fetch', async () => {
const queryCache = makeQueryCache();
const fetchFn = () => Promise.resolve('fresh')
const first = await queryCache.prefetchQuery('key', fetchFn, {
initialData: 'initial',
Expand All @@ -43,6 +43,7 @@ describe('queryCache', () => {
})

test('prefetchQuery should throw error when throwOnError is true', async () => {
const queryCache = makeQueryCache();
const fetchFn = () =>
new Promise(() => {
throw new Error('error')
Expand All @@ -57,6 +58,7 @@ describe('queryCache', () => {
})

test('should notify listeners when new query is added', () => {
const queryCache = makeQueryCache();
const callback = jest.fn()

queryCache.subscribe(callback)
Expand All @@ -67,6 +69,7 @@ describe('queryCache', () => {
})

test('should notify subsribers when new query with initialData is added', () => {
const queryCache = makeQueryCache();
const callback = jest.fn()

queryCache.subscribe(callback)
Expand All @@ -77,18 +80,21 @@ describe('queryCache', () => {
})

test('setQueryData creates a new query if query was not found, using exact', () => {
const queryCache = makeQueryCache();
queryCache.setQueryData('foo', 'bar', { exact: true })

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

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

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

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

// check the query was added to the cache
Expand All @@ -103,6 +109,7 @@ describe('queryCache', () => {
})

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

expect(queryCache.getQuery('key').state.data).toEqual('test data')
Expand All @@ -118,6 +125,7 @@ describe('queryCache', () => {
})

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

queryCache.setQueryData('updater', 'test data')
Expand All @@ -130,6 +138,7 @@ describe('queryCache', () => {
})

test('getQueries should return queries that partially match queryKey', async () => {
const queryCache = makeQueryCache();
const fetchData1 = () => Promise.resolve('data1')
const fetchData2 = () => Promise.resolve('data2')
const fetchDifferentData = () => Promise.resolve('data3')
Expand All @@ -142,6 +151,7 @@ describe('queryCache', () => {
})

test('stale timeout dispatch is not called if query is no longer in the query cache', async () => {
const queryCache = makeQueryCache();
const queryKey = 'key'
const fetchData = () => Promise.resolve('data')
await queryCache.prefetchQuery(queryKey, fetchData)
Expand All @@ -153,6 +163,7 @@ describe('queryCache', () => {
})

test('query is garbage collected when unsubscribed to', async () => {
const queryCache = makeQueryCache();
const queryKey = 'key'
const fetchData = () => Promise.resolve('data')
await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
Expand All @@ -166,6 +177,7 @@ describe('queryCache', () => {
})

test('query is not garbage collected unless markedForGarbageCollection is true', async () => {
const queryCache = makeQueryCache();
const queryKey = 'key'
const fetchData = () => Promise.resolve(undefined)
await queryCache.prefetchQuery(queryKey, fetchData, { cacheTime: 0 })
Expand Down
19 changes: 11 additions & 8 deletions src/tests/suspense.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { render, waitForElement, cleanup } from '@testing-library/react'
import * as React from 'react'

import { useQuery, queryCache } from '../index'
import { useQuery, ReactQueryCacheProvider } from '../index'
import { sleep } from './utils'

describe("useQuery's in Suspense mode", () => {
afterEach(() => {
queryCache.clear()
cleanup()
})

Expand All @@ -21,9 +20,11 @@ describe("useQuery's in Suspense mode", () => {
}

const rendered = render(
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
<ReactQueryCacheProvider>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
</ReactQueryCacheProvider>
)

await waitForElement(() => rendered.getByText('rendered'))
Expand All @@ -44,9 +45,11 @@ describe("useQuery's in Suspense mode", () => {
}

const rendered = render(
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
<ReactQueryCacheProvider>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
</ReactQueryCacheProvider>
)

await waitForElement(() => rendered.getByText('rendered'))
Expand Down

0 comments on commit 66777c6

Please sign in to comment.