Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jun 2, 2020
2 parents 4f56c31 + 342c1fb commit 3a589bb
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 170 deletions.
21 changes: 11 additions & 10 deletions README.md
Expand Up @@ -85,7 +85,6 @@ A big thanks to both [Draqula](https://github.com/vadimdemedes/draqula) for insp
[Zeit's SWR](https://github.com/zeit/swr) is a great library, and is very similar in spirit and implementation to React Query with a few notable differences:

- Automatic Cache Garbage Collection - React Query handles automatic cache purging for inactive queries and garbage collection. This can mean a much smaller memory footprint for apps that consume a lot of data or data that is changing often in a single session
- No Default Data Fetcher Function - React Query does not ship with a default fetcher (but can easily be wrapped inside of a custom hook to achieve the same functionality)
- `useMutation` - A dedicated hook for handling generic lifecycles around triggering mutations and handling their side-effects in applications. SWR does not ship with anything similar, and you may find yourself reimplementing most if not all of `useMutation`'s functionality in user-land. With this hook, you can extend the lifecycle of your mutations to reliably handle successful refetching strategies, failure rollbacks and error handling.
- Prefetching - React Query ships with 1st class prefetching utilities which not only come in handy with non-suspenseful apps but also make fetch-as-you-render patterns possible with React Query. SWR does not come with similar utilities and relies on `<link rel='preload'>` and/or manually fetching and updating the query cache
- Query cancellation integration is baked into React Query. You can easily use this to wire up request cancellation in most popular fetching libraries, including but not limited to fetch and axios.
Expand Down Expand Up @@ -164,8 +163,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always
<tbody>
<tr>
<td>
<a href="https://nozzle.io" target="_blank">
<img width='225' src="https://nozzle.io/img/logo-blue.png">
<a href="https://www.reactbricks.com/" target="_blank">
<img width='225' src="https://www.reactbricks.com/reactbricks_vertical.svg">
</a>
</td>
</tr>
Expand All @@ -178,8 +177,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always
<tbody>
<tr>
<td>
<a href="https://www.reactbricks.com/" target="_blank">
<img width='150' src="https://www.reactbricks.com/reactbricks_vertical.svg">
<a href="https://nozzle.io" target="_blank">
<img width='150' src="https://nozzle.io/img/logo-blue.png">
</a>
</td>
</tr>
Expand Down Expand Up @@ -485,7 +484,7 @@ To do this, you can use the following 2 approaches:

### Pass a falsy query key

If a query isn't ready to be requested yet, just pass a falsy value as the query key or as an item in the query key:
If a query isn't ready to be requested yet, just pass a falsy value as the query key:

```js
// Get the user
Expand Down Expand Up @@ -828,7 +827,7 @@ const { status, data, error } = useQuery('todos', fetchTodoList, {

## Prefetching

If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, then you're in luck. You can either use the `prefetchQuery` function to prefetch the results of a query to be placed into the cache:
If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the `prefetchQuery` function to prefetch the results of a query to be placed into the cache:

```js
import { queryCache } from 'react-query'
Expand All @@ -843,7 +842,7 @@ const prefetchTodos = async () => {

The next time a `useQuery` instance is used for a prefetched query, it will use the cached data! If no instances of `useQuery` appear for a prefetched query, it will be deleted and garbage collected after the time specified in `cacheTime`.

Alternatively, if you already have the data for your query synchronously available, you can use the [Query Cache's `setQueryData` method](#querycachesetquerydata) to directly add or update a query's cached result
Alternatively, if you already have the data for your query synchronously available, you can use the [Query Cache's `setQueryData` method](#querycachesetquerydata) to directly add or update a query's cached result.

## Initial Data

Expand Down Expand Up @@ -1102,7 +1101,7 @@ const CreateTodo = () => {
}
```
Even with just variables, mutations aren't all that special, but when used with the `onSuccess` option, the [Query Cache's `refetchQueries` method](#querycacherefetchqueries) method and the [Query Cache's `setQueryData` method](#querycachesetquerydata), mutations become a very powerful tool.
Even with just variables, mutations aren't all that special, but when used with the `onSuccess` option, the [Query Cache's `refetchQueries` method](#querycacherefetchqueries) and the [Query Cache's `setQueryData` method](#querycachesetquerydata), mutations become a very powerful tool.
Note that since version 1.1.0, the `mutate` function is no longer called synchronously so you cannot use it in an event callback. If you need to access the event in `onSubmit` you need to wrap `mutate` in another function. This is due to [React event pooling](https://reactjs.org/docs/events.html#event-pooling).
Expand Down Expand Up @@ -1992,7 +1991,7 @@ const {
- `loading` if the query is in an initial loading state. This means there is no cached data and the query is currently fetching, eg `isFetching === true`)
- `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch
- `success` if the query has received a response with no errors and is ready to display its data. The corresponding `data` property on the query is the data received from the successful fetch or if the query is in `manual` mode and has not been fetched yet `data` is the first `initialData` supplied to the query on initialization.
- `resolveData: Any`
- `resolvedData: Any`
- Defaults to `undefined`.
- The last successfully resolved data for the query.
- When fetching based on a new query key, the value will resolve to the last known successful value, regardless of query key
Expand Down Expand Up @@ -2028,6 +2027,8 @@ const {
isFetching,
failureCount,
refetch,
fetchMore,
canFetchMore,
} = useInfiniteQuery(queryKey, [, queryVariables], queryFn, {
getFetchMore: (lastPage, allPages) => fetchMoreVariable
manual,
Expand Down
53 changes: 34 additions & 19 deletions src/config.js
Expand Up @@ -3,26 +3,28 @@ import { noop, stableStringify, identity, deepEqual } from './utils'

export const configContext = React.createContext()

const DEFAULTS = {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 0,
cacheTime: 5 * 60 * 1000,
refetchAllOnWindowFocus: true,
refetchInterval: false,
suspense: false,
queryKeySerializerFn: defaultQueryKeySerializerFn,
queryFnParamsFilter: identity,
throwOnError: false,
useErrorBoundary: undefined, // this will default to the suspense value
onMutate: noop,
onSuccess: noop,
onError: noop,
onSettled: noop,
refetchOnMount: true,
isDataEqual: deepEqual,
}

export const defaultConfigRef = {
current: {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 0,
cacheTime: 5 * 60 * 1000,
refetchAllOnWindowFocus: true,
refetchInterval: false,
suspense: false,
queryKeySerializerFn: defaultQueryKeySerializerFn,
queryFnParamsFilter: identity,
throwOnError: false,
useErrorBoundary: undefined, // this will default to the suspense value
onMutate: noop,
onSuccess: noop,
onError: noop,
onSettled: noop,
refetchOnMount: true,
isDataEqual: deepEqual,
},
current: DEFAULTS,
}

export function useConfigContext() {
Expand All @@ -46,6 +48,19 @@ export function ReactQueryConfigProvider({ config, children }) {
return newConfig
}, [config, configContextValue])

React.useEffect(() => {
// restore previous config on unmount
return () => {
defaultConfigRef.current = { ...(configContextValue || DEFAULTS) }

// Default useErrorBoundary to the suspense value
if (typeof defaultConfigRef.current.useErrorBoundary === 'undefined') {
defaultConfigRef.current.useErrorBoundary =
defaultConfigRef.current.suspense
}
}
}, [configContextValue])

if (!configContextValue) {
defaultConfigRef.current = newConfig
}
Expand Down
7 changes: 7 additions & 0 deletions src/queryCache.js
Expand Up @@ -84,6 +84,7 @@ export function makeQueryCache() {
}

cache.clear = () => {
Object.values(cache.queries).forEach(query => query.clear())
cache.queries = {}
notifyGlobalListeners()
}
Expand Down Expand Up @@ -516,6 +517,12 @@ export function makeQueryCache() {
query.scheduleStaleTimeout()
}

query.clear = () => {
clearTimeout(query.staleTimeout)
clearTimeout(query.cacheTimeout)
query.cancel()
}

return query
}

Expand Down
156 changes: 153 additions & 3 deletions src/tests/config.test.js
@@ -1,16 +1,23 @@
import React from 'react'
import { render, waitForElement, cleanup } from '@testing-library/react'
import React, { useState } from 'react'
import {
act,
fireEvent,
render,
waitForElement,
cleanup,
} from '@testing-library/react'
import {
ReactQueryConfigProvider,
useQuery,
queryCache,
ReactQueryCacheProvider,
} from '../index'

import { sleep } from './utils'

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

// See https://github.com/tannerlinsley/react-query/issues/105
Expand Down Expand Up @@ -46,4 +53,147 @@ describe('config', () => {

expect(onSuccess).toHaveBeenCalledWith('data')
})

it('should reset to defaults when all providers are unmounted', async () => {
const onSuccess = jest.fn()

const config = {
refetchAllOnWindowFocus: false,
refetchOnMount: false,
retry: false,
manual: true,
}

const queryFn = async () => {
await sleep(10)
return 'data'
}

function Page() {
const { data } = useQuery('test', queryFn)

return (
<div>
<h1>Data: {data || 'none'}</h1>
</div>
)
}

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

await rendered.findByText('Data: none')

act(() => {
queryCache.prefetchQuery('test', queryFn, { force: true })
})

await rendered.findByText('Data: data')

// tear down and unmount
cleanup()

// wipe query cache/stored config
act(() => queryCache.clear())
onSuccess.mockClear()

// rerender WITHOUT config provider,
// so we are NOT passing the config above (refetchOnMount should be `true` by default)
const rerendered = render(<Page />)

await rerendered.findByText('Data: data')
})

it('should reset to previous config when nested provider is unmounted', async () => {
let counterRef = 0
const parentOnSuccess = jest.fn()

const parentConfig = {
refetchOnMount: false,
onSuccess: parentOnSuccess,
}

const childConfig = {
refetchOnMount: true,

// Override onSuccess of parent, making it a no-op
onSuccess: undefined,
}

const queryFn = async () => {
await sleep(10)
counterRef += 1
return String(counterRef)
}

function Component() {
const { data, refetch } = useQuery('test', queryFn)

return (
<div>
<h1>Data: {data}</h1>
<button data-testid="refetch" onClick={() => refetch()}>
Refetch
</button>
</div>
)
}

function Page() {
const [childConfigEnabled, setChildConfigEnabled] = useState(true)

return (
<div>
{childConfigEnabled && (
<ReactQueryConfigProvider config={childConfig}>
<Component />
</ReactQueryConfigProvider>
)}
{!childConfigEnabled && <Component />}
<button
data-testid="disableChildConfig"
onClick={() => setChildConfigEnabled(false)}
>
Disable Child Config
</button>
</div>
)
}

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

await rendered.findByText('Data: 1')

expect(parentOnSuccess).not.toHaveBeenCalled()

fireEvent.click(rendered.getByTestId('refetch'))

await rendered.findByText('Data: 2')

expect(parentOnSuccess).not.toHaveBeenCalled()

parentOnSuccess.mockReset()

fireEvent.click(rendered.getByTestId('disableChildConfig'))

await rendered.findByText('Data: 2')

// it should not refetch on mount
expect(parentOnSuccess).not.toHaveBeenCalled()

fireEvent.click(rendered.getByTestId('refetch'))

await rendered.findByText('Data: 3')

expect(parentOnSuccess).toHaveBeenCalledTimes(1)
})
})
7 changes: 6 additions & 1 deletion src/tests/suspense.test.js
@@ -1,4 +1,9 @@
import { render, waitForElement, fireEvent, cleanup } from '@testing-library/react'
import {
render,
waitForElement,
fireEvent,
cleanup,
} from '@testing-library/react'
import * as React from 'react'

import { useQuery, ReactQueryCacheProvider, queryCache } from '../index'
Expand Down
35 changes: 35 additions & 0 deletions src/tests/usePaginatedQuery.test.js
Expand Up @@ -192,4 +192,39 @@ describe('usePaginatedQuery', () => {
rendered.getByText('Data second-search 1')
await waitForElement(() => rendered.getByText('Data second-search 2'))
})

it('should not suspend while fetching the next page', async () => {
function Page() {
const [page, setPage] = React.useState(1)

const { resolvedData } = usePaginatedQuery(
['data', { page }],
async (queryName, { page }) => {
await sleep(1)
return page
},
{
initialData: 0,
suspense: true,
}
)

return (
<div>
<h1 data-testid="title">Data {resolvedData}</h1>
<button onClick={() => setPage(page + 1)}>next</button>
</div>
)
}

// render will throw if Page is suspended
const rendered = render(
<ReactQueryCacheProvider>
<Page />
</ReactQueryCacheProvider>
)

fireEvent.click(rendered.getByText('next'))
await waitForElement(() => rendered.getByText('Data 2'))
})
})

0 comments on commit 3a589bb

Please sign in to comment.