Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: query cache provider #476

Merged
merged 6 commits into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,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 @@ -2522,6 +2524,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 @@ -2582,6 +2596,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 React Query. 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
41 changes: 40 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,42 @@ import { defaultConfigRef } from './config'

export const queryCache = makeQueryCache()

export const queryCacheContext = React.createContext(queryCache)

export const queryCaches = [queryCache]

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

export function ReactQueryCacheProvider({ queryCache, children }) {
tannerlinsley marked this conversation as resolved.
Show resolved Hide resolved
const cache = React.useMemo(() => queryCache || makeQueryCache(), [
queryCache,
])

React.useEffect(() => {
queryCaches.push(cache)

return () => {
// remove the cache from the active list
const i = queryCaches.indexOf(cache)
if (i >= 0) {
queryCaches.splice(i, 1)
}
// if the cache was created by us, we need to tear it down
if (queryCache == null) {
cache.clear()
}
}
}, [cache, queryCache])

return (
<queryCacheContext.Provider value={cache}>
{children}
</queryCacheContext.Provider>
)
}

const actionInit = {}
const actionFailed = {}
const actionMarkStale = {}
Expand All @@ -32,7 +69,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 @@ -129,6 +166,7 @@ export function makeQueryCache() {
query.config = { ...query.config, ...config }
} else {
query = makeQuery({
cache,
queryKey,
queryHash,
queryVariables,
Expand Down Expand Up @@ -212,6 +250,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
50 changes: 26 additions & 24 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 { queryCaches } from './queryCache'

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

if (isDocumentVisible() && isOnline()) {
queryCache
.refetchQueries(query => {
if (!query.instances.length) {
return false
}

if (query.config.manual === true) {
return false
}

if (query.shouldContinueRetryOnFocus) {
// delete promise, so `fetch` will create new one
delete query.promise
return true
}

if (typeof query.config.refetchOnWindowFocus === 'undefined') {
return refetchAllOnWindowFocus
} else {
return query.config.refetchOnWindowFocus
}
})
.catch(Console.error)
queryCaches.forEach(queryCache =>
queryCache
.refetchQueries(query => {
if (!query.instances.length) {
return false
}

if (query.config.manual === true) {
return false
}

if (query.shouldContinueRetryOnFocus) {
// delete promise, so `fetch` will create new one
delete query.promise
return true
}

if (typeof query.config.refetchOnWindowFocus === 'undefined') {
return refetchAllOnWindowFocus
} else {
return query.config.refetchOnWindowFocus
}
})
.catch(Console.error)
)
}
}

Expand Down
177 changes: 177 additions & 0 deletions src/tests/ReactQueryCacheProvider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React, { useEffect } from 'react'
import { render, cleanup, waitForElement } from '@testing-library/react'
import {
ReactQueryCacheProvider,
makeQueryCache,
queryCache,
useQuery,
useQueryCache,
} from '../index'
import { sleep } from './utils'

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

test('when not used, falls back to global cache', async () => {
function Page() {
const { data } = useQuery('test', async () => {
await sleep(10)
return 'test'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}

const rendered = render(<Page />)

await waitForElement(() => rendered.getByText('test'))

expect(queryCache.getQuery('test')).toBeDefined()
})
test('sets a specific cache for all queries to use', async () => {
const cache = makeQueryCache()

function Page() {
const { data } = useQuery('test', async () => {
await sleep(10)
return 'test'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}

const rendered = render(
<ReactQueryCacheProvider queryCache={cache}>
<Page />
</ReactQueryCacheProvider>
)

await waitForElement(() => rendered.getByText('test'))

expect(queryCache.getQuery('test')).not.toBeDefined()
expect(cache.getQuery('test')).toBeDefined()
})
test('implicitly creates a new cache for all queries to use', async () => {
function Page() {
const { data } = useQuery('test', async () => {
await sleep(10)
return 'test'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}

const rendered = render(
<ReactQueryCacheProvider>
<Page />
</ReactQueryCacheProvider>
)

await waitForElement(() => rendered.getByText('test'))

expect(queryCache.getQuery('test')).not.toBeDefined()
})
test('allows multiple caches to be partitioned', async () => {
const cache1 = makeQueryCache()
const cache2 = makeQueryCache()

function Page1() {
const { data } = useQuery('test1', async () => {
await sleep(10)
return 'test1'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}
function Page2() {
const { data } = useQuery('test2', async () => {
await sleep(10)
return 'test2'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}

const rendered = render(
<>
<ReactQueryCacheProvider queryCache={cache1}>
<Page1 />
</ReactQueryCacheProvider>
<ReactQueryCacheProvider queryCache={cache2}>
<Page2 />
</ReactQueryCacheProvider>
</>
)

await waitForElement(() => rendered.getByText('test1'))
await waitForElement(() => rendered.getByText('test2'))

expect(cache1.getQuery('test1')).toBeDefined()
expect(cache1.getQuery('test2')).not.toBeDefined()
expect(cache2.getQuery('test1')).not.toBeDefined()
expect(cache2.getQuery('test2')).toBeDefined()
})
test('when cache changes, previous cache is cleaned', () => {
let caches = []
const customCache = makeQueryCache()

function Page() {
const queryCache = useQueryCache()
useEffect(() => {
caches.push(queryCache)
}, [queryCache])

const { data } = useQuery('test', async () => {
await sleep(10)
return 'test'
})

return (
<div>
<h1>{data}</h1>
</div>
)
}

function App({ cache }) {
return (
<ReactQueryCacheProvider queryCache={cache}>
<Page />
</ReactQueryCacheProvider>
)
}

const rendered = render(<App />)

expect(caches).toHaveLength(1)
jest.spyOn(caches[0], 'clear')

rendered.rerender(<App cache={customCache} />)

expect(caches).toHaveLength(2)
expect(caches[0].clear).toHaveBeenCalled()
})
})