Skip to content

Commit 3a589bb

Browse files
committed
2 parents 4f56c31 + 342c1fb commit 3a589bb

File tree

10 files changed

+474
-170
lines changed

10 files changed

+474
-170
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ A big thanks to both [Draqula](https://github.com/vadimdemedes/draqula) for insp
8585
[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:
8686

8787
- 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
88-
- 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)
8988
- `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.
9089
- 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
9190
- 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.
@@ -164,8 +163,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always
164163
<tbody>
165164
<tr>
166165
<td>
167-
<a href="https://nozzle.io" target="_blank">
168-
<img width='225' src="https://nozzle.io/img/logo-blue.png">
166+
<a href="https://www.reactbricks.com/" target="_blank">
167+
<img width='225' src="https://www.reactbricks.com/reactbricks_vertical.svg">
169168
</a>
170169
</td>
171170
</tr>
@@ -178,8 +177,8 @@ This library is being built and maintained by me, @tannerlinsley and I am always
178177
<tbody>
179178
<tr>
180179
<td>
181-
<a href="https://www.reactbricks.com/" target="_blank">
182-
<img width='150' src="https://www.reactbricks.com/reactbricks_vertical.svg">
180+
<a href="https://nozzle.io" target="_blank">
181+
<img width='150' src="https://nozzle.io/img/logo-blue.png">
183182
</a>
184183
</td>
185184
</tr>
@@ -485,7 +484,7 @@ To do this, you can use the following 2 approaches:
485484

486485
### Pass a falsy query key
487486

488-
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:
487+
If a query isn't ready to be requested yet, just pass a falsy value as the query key:
489488

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

829828
## Prefetching
830829

831-
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:
830+
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:
832831

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

844843
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`.
845844

846-
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
845+
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.
847846

848847
## Initial Data
849848

@@ -1102,7 +1101,7 @@ const CreateTodo = () => {
11021101
}
11031102
```
11041103
1105-
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.
1104+
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.
11061105
11071106
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).
11081107
@@ -1992,7 +1991,7 @@ const {
19921991
- `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`)
19931992
- `error` if the query attempt resulted in an error. The corresponding `error` property has the error received from the attempted fetch
19941993
- `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.
1995-
- `resolveData: Any`
1994+
- `resolvedData: Any`
19961995
- Defaults to `undefined`.
19971996
- The last successfully resolved data for the query.
19981997
- When fetching based on a new query key, the value will resolve to the last known successful value, regardless of query key
@@ -2028,6 +2027,8 @@ const {
20282027
isFetching,
20292028
failureCount,
20302029
refetch,
2030+
fetchMore,
2031+
canFetchMore,
20312032
} = useInfiniteQuery(queryKey, [, queryVariables], queryFn, {
20322033
getFetchMore: (lastPage, allPages) => fetchMoreVariable
20332034
manual,

src/config.js

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ import { noop, stableStringify, identity, deepEqual } from './utils'
33

44
export const configContext = React.createContext()
55

6+
const DEFAULTS = {
7+
retry: 3,
8+
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
9+
staleTime: 0,
10+
cacheTime: 5 * 60 * 1000,
11+
refetchAllOnWindowFocus: true,
12+
refetchInterval: false,
13+
suspense: false,
14+
queryKeySerializerFn: defaultQueryKeySerializerFn,
15+
queryFnParamsFilter: identity,
16+
throwOnError: false,
17+
useErrorBoundary: undefined, // this will default to the suspense value
18+
onMutate: noop,
19+
onSuccess: noop,
20+
onError: noop,
21+
onSettled: noop,
22+
refetchOnMount: true,
23+
isDataEqual: deepEqual,
24+
}
25+
626
export const defaultConfigRef = {
7-
current: {
8-
retry: 3,
9-
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
10-
staleTime: 0,
11-
cacheTime: 5 * 60 * 1000,
12-
refetchAllOnWindowFocus: true,
13-
refetchInterval: false,
14-
suspense: false,
15-
queryKeySerializerFn: defaultQueryKeySerializerFn,
16-
queryFnParamsFilter: identity,
17-
throwOnError: false,
18-
useErrorBoundary: undefined, // this will default to the suspense value
19-
onMutate: noop,
20-
onSuccess: noop,
21-
onError: noop,
22-
onSettled: noop,
23-
refetchOnMount: true,
24-
isDataEqual: deepEqual,
25-
},
27+
current: DEFAULTS,
2628
}
2729

2830
export function useConfigContext() {
@@ -46,6 +48,19 @@ export function ReactQueryConfigProvider({ config, children }) {
4648
return newConfig
4749
}, [config, configContextValue])
4850

51+
React.useEffect(() => {
52+
// restore previous config on unmount
53+
return () => {
54+
defaultConfigRef.current = { ...(configContextValue || DEFAULTS) }
55+
56+
// Default useErrorBoundary to the suspense value
57+
if (typeof defaultConfigRef.current.useErrorBoundary === 'undefined') {
58+
defaultConfigRef.current.useErrorBoundary =
59+
defaultConfigRef.current.suspense
60+
}
61+
}
62+
}, [configContextValue])
63+
4964
if (!configContextValue) {
5065
defaultConfigRef.current = newConfig
5166
}

src/queryCache.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export function makeQueryCache() {
8484
}
8585

8686
cache.clear = () => {
87+
Object.values(cache.queries).forEach(query => query.clear())
8788
cache.queries = {}
8889
notifyGlobalListeners()
8990
}
@@ -516,6 +517,12 @@ export function makeQueryCache() {
516517
query.scheduleStaleTimeout()
517518
}
518519

520+
query.clear = () => {
521+
clearTimeout(query.staleTimeout)
522+
clearTimeout(query.cacheTimeout)
523+
query.cancel()
524+
}
525+
519526
return query
520527
}
521528

src/tests/config.test.js

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import React from 'react'
2-
import { render, waitForElement, cleanup } from '@testing-library/react'
1+
import React, { useState } from 'react'
2+
import {
3+
act,
4+
fireEvent,
5+
render,
6+
waitForElement,
7+
cleanup,
8+
} from '@testing-library/react'
39
import {
410
ReactQueryConfigProvider,
511
useQuery,
12+
queryCache,
613
ReactQueryCacheProvider,
714
} from '../index'
815

916
import { sleep } from './utils'
1017

1118
describe('config', () => {
1219
afterEach(() => {
13-
cleanup()
20+
queryCache.clear()
1421
})
1522

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

4754
expect(onSuccess).toHaveBeenCalledWith('data')
4855
})
56+
57+
it('should reset to defaults when all providers are unmounted', async () => {
58+
const onSuccess = jest.fn()
59+
60+
const config = {
61+
refetchAllOnWindowFocus: false,
62+
refetchOnMount: false,
63+
retry: false,
64+
manual: true,
65+
}
66+
67+
const queryFn = async () => {
68+
await sleep(10)
69+
return 'data'
70+
}
71+
72+
function Page() {
73+
const { data } = useQuery('test', queryFn)
74+
75+
return (
76+
<div>
77+
<h1>Data: {data || 'none'}</h1>
78+
</div>
79+
)
80+
}
81+
82+
const rendered = render(
83+
<ReactQueryConfigProvider config={config}>
84+
<Page />
85+
</ReactQueryConfigProvider>
86+
)
87+
88+
await rendered.findByText('Data: none')
89+
90+
act(() => {
91+
queryCache.prefetchQuery('test', queryFn, { force: true })
92+
})
93+
94+
await rendered.findByText('Data: data')
95+
96+
// tear down and unmount
97+
cleanup()
98+
99+
// wipe query cache/stored config
100+
act(() => queryCache.clear())
101+
onSuccess.mockClear()
102+
103+
// rerender WITHOUT config provider,
104+
// so we are NOT passing the config above (refetchOnMount should be `true` by default)
105+
const rerendered = render(<Page />)
106+
107+
await rerendered.findByText('Data: data')
108+
})
109+
110+
it('should reset to previous config when nested provider is unmounted', async () => {
111+
let counterRef = 0
112+
const parentOnSuccess = jest.fn()
113+
114+
const parentConfig = {
115+
refetchOnMount: false,
116+
onSuccess: parentOnSuccess,
117+
}
118+
119+
const childConfig = {
120+
refetchOnMount: true,
121+
122+
// Override onSuccess of parent, making it a no-op
123+
onSuccess: undefined,
124+
}
125+
126+
const queryFn = async () => {
127+
await sleep(10)
128+
counterRef += 1
129+
return String(counterRef)
130+
}
131+
132+
function Component() {
133+
const { data, refetch } = useQuery('test', queryFn)
134+
135+
return (
136+
<div>
137+
<h1>Data: {data}</h1>
138+
<button data-testid="refetch" onClick={() => refetch()}>
139+
Refetch
140+
</button>
141+
</div>
142+
)
143+
}
144+
145+
function Page() {
146+
const [childConfigEnabled, setChildConfigEnabled] = useState(true)
147+
148+
return (
149+
<div>
150+
{childConfigEnabled && (
151+
<ReactQueryConfigProvider config={childConfig}>
152+
<Component />
153+
</ReactQueryConfigProvider>
154+
)}
155+
{!childConfigEnabled && <Component />}
156+
<button
157+
data-testid="disableChildConfig"
158+
onClick={() => setChildConfigEnabled(false)}
159+
>
160+
Disable Child Config
161+
</button>
162+
</div>
163+
)
164+
}
165+
166+
const rendered = render(
167+
<ReactQueryConfigProvider config={parentConfig}>
168+
<ReactQueryCacheProvider>
169+
<Page />
170+
</ReactQueryCacheProvider>
171+
</ReactQueryConfigProvider>
172+
)
173+
174+
await rendered.findByText('Data: 1')
175+
176+
expect(parentOnSuccess).not.toHaveBeenCalled()
177+
178+
fireEvent.click(rendered.getByTestId('refetch'))
179+
180+
await rendered.findByText('Data: 2')
181+
182+
expect(parentOnSuccess).not.toHaveBeenCalled()
183+
184+
parentOnSuccess.mockReset()
185+
186+
fireEvent.click(rendered.getByTestId('disableChildConfig'))
187+
188+
await rendered.findByText('Data: 2')
189+
190+
// it should not refetch on mount
191+
expect(parentOnSuccess).not.toHaveBeenCalled()
192+
193+
fireEvent.click(rendered.getByTestId('refetch'))
194+
195+
await rendered.findByText('Data: 3')
196+
197+
expect(parentOnSuccess).toHaveBeenCalledTimes(1)
198+
})
49199
})

src/tests/suspense.test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { render, waitForElement, fireEvent, cleanup } from '@testing-library/react'
1+
import {
2+
render,
3+
waitForElement,
4+
fireEvent,
5+
cleanup,
6+
} from '@testing-library/react'
27
import * as React from 'react'
38

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

src/tests/usePaginatedQuery.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,39 @@ describe('usePaginatedQuery', () => {
192192
rendered.getByText('Data second-search 1')
193193
await waitForElement(() => rendered.getByText('Data second-search 2'))
194194
})
195+
196+
it('should not suspend while fetching the next page', async () => {
197+
function Page() {
198+
const [page, setPage] = React.useState(1)
199+
200+
const { resolvedData } = usePaginatedQuery(
201+
['data', { page }],
202+
async (queryName, { page }) => {
203+
await sleep(1)
204+
return page
205+
},
206+
{
207+
initialData: 0,
208+
suspense: true,
209+
}
210+
)
211+
212+
return (
213+
<div>
214+
<h1 data-testid="title">Data {resolvedData}</h1>
215+
<button onClick={() => setPage(page + 1)}>next</button>
216+
</div>
217+
)
218+
}
219+
220+
// render will throw if Page is suspended
221+
const rendered = render(
222+
<ReactQueryCacheProvider>
223+
<Page />
224+
</ReactQueryCacheProvider>
225+
)
226+
227+
fireEvent.click(rendered.getByText('next'))
228+
await waitForElement(() => rendered.getByText('Data 2'))
229+
})
195230
})

0 commit comments

Comments
 (0)