Skip to content

Commit 44f6d27

Browse files
committed
Merge remote-tracking branch 'react-query/v4' into 2919-query-key-array
2 parents ffb9857 + a090fe5 commit 44f6d27

File tree

7 files changed

+86
-7
lines changed

7 files changed

+86
-7
lines changed

docs/src/pages/guides/migrating-to-react-query-4.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ If you were importing anything from `'react-query/react'` directly in your proje
145145
+ import { QueryClientProvider } from 'react-query/reactjs';
146146
```
147147

148+
### `onSuccess` is no longer called from `setQueryData`
149+
150+
This was confusing to many and also created infinite loops if `setQueryData` was called from within `onSuccess`. It was also a frequent source of error when combined with `staleTime`, because if data was read from the cache only, `onSuccess` was _not_ called.
151+
152+
Similar to `onError` and `onSettled`, the `onSuccess` callback is now tied to a request being made. No request -> no callback.
153+
154+
If you want to listen to changes of the `data` field, you can best do this with a `useEffect`, where `data` is part of the dependency Array. Since react-query ensures stable data through structural sharing, the effect will not execute with every background refetch, but only if something within data has changed:
155+
156+
```
157+
const { data } = useQuery({ queryKey, queryFn })
158+
React.useEffect(() => mySideEffectHere(data), [data])
159+
```
160+
148161
### `persistQueryClient` and the corresponding persister plugins are no longer experimental and have been renamed
149162

150163
The plugins `createWebStoragePersistor` and `createAsyncStoragePersistor` have been renamed to [`createWebStoragePersister`](/plugins/createWebStoragePersister) and [`createAsyncStoragePersister`](/plugins/createAsyncStoragePersister) respectively. The interface `Persistor` in `persistQueryClient` has also been renamed to `Persister`. Checkout [this stackexchange](https://english.stackexchange.com/questions/206893/persister-or-persistor) for the motivation of this change.

docs/src/pages/reference/QueryClient.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,6 @@ This distinction is more a "convenience" for ts devs that know which structure w
205205

206206
`setQueryData` is a synchronous function that can be used to immediately update a query's cached data. If the query does not exist, it will be created. **If the query is not utilized by a query hook in the default `cacheTime` of 5 minutes, the query will be garbage collected**.
207207

208-
After successful changing query's cached data via `setQueryData`, it will also trigger `onSuccess` callback from that query.
209-
210208
> The difference between using `setQueryData` and `fetchQuery` is that `setQueryData` is sync and assumes that you already synchronously have the data available. If you need to fetch the data asynchronously, it's suggested that you either refetch the query key or use `fetchQuery` to handle the asynchronous fetch.
211209
212210
```js

docs/src/pages/reference/useQuery.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const result = useQuery({
136136
- If set to `['isStale']` for example, the component will not re-render when the `isStale` property changes.
137137
- `onSuccess: (data: TData) => void`
138138
- Optional
139-
- This function will fire any time the query successfully fetches new data or the cache is updated via `setQueryData`.
139+
- This function will fire any time the query successfully fetches new data.
140140
- `onError: (error: TError) => void`
141141
- Optional
142142
- This function will fire if the query encounters an error and will be passed the error.

src/core/query.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ interface SuccessAction<TData> {
9898
data: TData | undefined
9999
type: 'success'
100100
dataUpdatedAt?: number
101+
notifySuccess?: boolean
101102
}
102103

103104
interface ErrorAction<TError> {
@@ -200,7 +201,7 @@ export class Query<
200201

201202
setData(
202203
updater: Updater<TData | undefined, TData>,
203-
options?: SetDataOptions
204+
options?: SetDataOptions & { notifySuccess: boolean }
204205
): TData {
205206
const prevData = this.state.data
206207

@@ -220,6 +221,7 @@ export class Query<
220221
data,
221222
type: 'success',
222223
dataUpdatedAt: options?.updatedAt,
224+
notifySuccess: options?.notifySuccess,
223225
})
224226

225227
return data

src/core/queryClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class QueryClient {
136136
const defaultedOptions = this.defaultQueryOptions(parsedOptions)
137137
return this.queryCache
138138
.build(this, defaultedOptions)
139-
.setData(updater, options)
139+
.setData(updater, { ...options, notifySuccess: false })
140140
}
141141

142142
setQueriesData<TData>(

src/core/queryObserver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ export class QueryObserver<
679679
const notifyOptions: NotifyOptions = {}
680680

681681
if (action.type === 'success') {
682-
notifyOptions.onSuccess = true
682+
notifyOptions.onSuccess = action.notifySuccess ?? true
683683
} else if (action.type === 'error' && !isCancelledError(action.error)) {
684684
notifyOptions.onError = true
685685
}

src/core/tests/queryClient.test.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { sleep, queryKey, mockConsoleError } from '../../reactjs/tests/utils'
1+
import { waitFor } from '@testing-library/react'
2+
import '@testing-library/jest-dom'
3+
import React from 'react'
4+
5+
import {
6+
sleep,
7+
queryKey,
8+
mockConsoleError,
9+
renderWithClient,
10+
} from '../../reactjs/tests/utils'
211
import {
12+
useQuery,
313
InfiniteQueryObserver,
414
QueryCache,
515
QueryClient,
@@ -206,6 +216,62 @@ describe('queryClient', () => {
206216

207217
expect(queryCache.find(key)!.state.data).toBe(newData)
208218
})
219+
220+
test('should not call onSuccess callback of active observers', async () => {
221+
const key = queryKey()
222+
const onSuccess = jest.fn()
223+
224+
function Page() {
225+
const state = useQuery(key, () => 'data', { onSuccess })
226+
return (
227+
<div>
228+
<div>data: {state.data}</div>
229+
<button onClick={() => queryClient.setQueryData(key, 'newData')}>
230+
setQueryData
231+
</button>
232+
</div>
233+
)
234+
}
235+
236+
const rendered = renderWithClient(queryClient, <Page />)
237+
238+
await waitFor(() => rendered.getByText('data: data'))
239+
rendered.getByRole('button', { name: /setQueryData/i }).click()
240+
await waitFor(() => rendered.getByText('data: newData'))
241+
242+
expect(onSuccess).toHaveBeenCalledTimes(1)
243+
expect(onSuccess).toHaveBeenCalledWith('data')
244+
})
245+
246+
test('should respect updatedAt', async () => {
247+
const key = queryKey()
248+
249+
function Page() {
250+
const state = useQuery(key, () => 'data')
251+
return (
252+
<div>
253+
<div>data: {state.data}</div>
254+
<div>dataUpdatedAt: {state.dataUpdatedAt}</div>
255+
<button
256+
onClick={() =>
257+
queryClient.setQueryData(key, 'newData', { updatedAt: 100 })
258+
}
259+
>
260+
setQueryData
261+
</button>
262+
</div>
263+
)
264+
}
265+
266+
const rendered = renderWithClient(queryClient, <Page />)
267+
268+
await waitFor(() => rendered.getByText('data: data'))
269+
rendered.getByRole('button', { name: /setQueryData/i }).click()
270+
await waitFor(() => rendered.getByText('data: newData'))
271+
await waitFor(() => {
272+
expect(rendered.getByText('dataUpdatedAt: 100')).toBeInTheDocument()
273+
})
274+
})
209275
})
210276

211277
describe('setQueriesData', () => {

0 commit comments

Comments
 (0)