Skip to content

Commit b3c9ce8

Browse files
authored
feat: add notifyOnStatusChange flag (#840)
1 parent 52607f6 commit b3c9ce8

File tree

11 files changed

+152
-61
lines changed

11 files changed

+152
-61
lines changed

docs/src/pages/docs/api.md

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,41 @@ title: API Reference
77

88
```js
99
const {
10-
status,
11-
isIdle,
12-
isLoading,
13-
isSuccess,
14-
isError,
10+
clear,
1511
data,
1612
error,
17-
isStale,
18-
isFetching,
1913
failureCount,
14+
isError,
15+
isFetching,
16+
isIdle,
17+
isLoading,
18+
isStale,
19+
isSuccess,
2020
refetch,
21-
clear,
21+
status,
2222
} = useQuery(queryKey, queryFn?, {
23-
suspense,
24-
queryKeySerializerFn,
25-
enabled,
26-
retry,
27-
retryDelay,
28-
staleTime,
2923
cacheTime,
24+
enabled,
25+
initialData,
26+
initialStale,
27+
isDataEqual,
3028
keepPreviousData,
31-
refetchOnWindowFocus,
32-
refetchOnReconnect,
29+
notifyOnStatusChange,
30+
onError,
31+
onSettled,
32+
onSuccess,
33+
queryFnParamsFilter,
34+
queryKeySerializerFn,
3335
refetchInterval,
3436
refetchIntervalInBackground,
35-
queryFnParamsFilter,
3637
refetchOnMount,
38+
refetchOnReconnect,
39+
refetchOnWindowFocus,
40+
retry,
41+
retryDelay,
42+
staleTime,
3743
structuralSharing,
38-
isDataEqual,
39-
onError,
40-
onSuccess,
41-
onSettled,
42-
initialData,
43-
initialStale,
44+
suspense,
4445
useErrorBoundary,
4546
})
4647

@@ -96,6 +97,11 @@ const queryInfo = useQuery({
9697
- `refetchOnReconnect: Boolean`
9798
- Optional
9899
- Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
100+
- `notifyOnStatusChange: Boolean`
101+
- Optional
102+
- Whether a change to the query status should re-render a component.
103+
- If set to `false`, the component will only re-render when the actual `data` or `error` changes.
104+
- Defaults to `true`.
99105
- `onSuccess: Function(data) => data`
100106
- Optional
101107
- This function will fire any time the query successfully fetches new data.

docs/src/pages/docs/comparison.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ Feature/Capability Key:
1919
| Supported Query Keys | JSON | JSON | GraphQL Query |
2020
| Query Key Change Detection | Deep Compare (Serialization) | Referential Equality (===) | Deep Compare (Serialization) |
2121
| Query Data Memoization Level | Query + Structural Sharing | Query | Query + Entity + Structural Sharing |
22-
| Stale While Revalidate | Server-Side + Client-Side | Server-Side | None |
2322
| Bundle Size | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr] | [![][bp-apollo]][bpl-apollo] |
2423
| Queries ||||
2524
| Caching ||||
@@ -38,6 +37,8 @@ Feature/Capability Key:
3837
| Prefetching APIs || 🔶 ||
3938
| Query Cancellation || 🛑 | 🛑 |
4039
| Partial Query Matching<sup>2</sup> || 🛑 | 🛑 |
40+
| Stale While Revalidate ||| 🛑 |
41+
| Stale Time Configuration || 🛑 | 🛑 |
4142
| Window Focus Refetching ||| 🛑 |
4243
| Network Status Refetching ||||
4344
| Automatic Refetch after Mutation<sup>3</sup> | 🔶 | 🔶 ||

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
6060
refetchOnWindowFocus: true,
6161
refetchOnReconnect: true,
6262
refetchOnMount: true,
63+
notifyOnStatusChange: true,
6364
structuralSharing: true,
6465
},
6566
}

src/core/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ export class Query<TResult, TError> {
323323
config = this.config
324324

325325
// Check if there is a query function
326-
if (!config.queryFn) {
326+
if (typeof config.queryFn !== 'function') {
327327
return
328328
}
329329

src/core/queryCache.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,6 @@ export class QueryCache {
323323
if (options?.throwOnError) {
324324
throw error
325325
}
326-
return
327326
}
328327
}
329328

src/core/queryObserver.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -217,22 +217,16 @@ export class QueryObserver<TResult, TError> {
217217
}
218218

219219
private createResult(): QueryResult<TResult, TError> {
220-
const { currentResult, currentQuery, previousResult, config } = this
221-
222-
const {
223-
canFetchMore,
224-
error,
225-
failureCount,
226-
isFetched,
227-
isFetching,
228-
isFetchingMore,
229-
isLoading,
230-
} = currentQuery.state
231-
232-
let { data, status, updatedAt } = currentQuery.state
220+
const { currentQuery, currentResult, previousResult, config } = this
221+
const { state } = currentQuery
222+
let { data, status, updatedAt } = state
233223

234224
// Keep previous data if needed
235-
if (config.keepPreviousData && isLoading && previousResult?.isSuccess) {
225+
if (
226+
config.keepPreviousData &&
227+
state.isLoading &&
228+
previousResult?.isSuccess
229+
) {
236230
data = previousResult.data
237231
updatedAt = previousResult.updatedAt
238232
status = previousResult.status
@@ -256,15 +250,15 @@ export class QueryObserver<TResult, TError> {
256250

257251
return {
258252
...getStatusProps(status),
259-
canFetchMore,
253+
canFetchMore: state.canFetchMore,
260254
clear: this.clear,
261255
data,
262-
error,
263-
failureCount,
256+
error: state.error,
257+
failureCount: state.failureCount,
264258
fetchMore: this.fetchMore,
265-
isFetched,
266-
isFetching,
267-
isFetchingMore,
259+
isFetched: state.isFetched,
260+
isFetching: state.isFetching,
261+
isFetchingMore: state.isFetchingMore,
268262
isStale,
269263
query: currentQuery,
270264
refetch: this.refetch,
@@ -304,20 +298,35 @@ export class QueryObserver<TResult, TError> {
304298
_state: QueryState<TResult, TError>,
305299
action: Action<TResult, TError>
306300
): void {
307-
this.currentResult = this.createResult()
301+
const { config } = this
308302

309-
const { data, error, isSuccess, isError } = this.currentResult
303+
// Store current result and get new result
304+
const prevResult = this.currentResult
305+
this.currentResult = this.createResult()
306+
const result = this.currentResult
310307

311-
if (action.type === 'Success' && isSuccess) {
312-
this.config.onSuccess?.(data!)
313-
this.config.onSettled?.(data!, null)
308+
// We need to check the action because the state could have
309+
// transitioned from success to success in case of `setQueryData`.
310+
if (action.type === 'Success' && result.isSuccess) {
311+
config.onSuccess?.(result.data!)
312+
config.onSettled?.(result.data!, null)
314313
this.updateTimers()
315-
} else if (action.type === 'Error' && isError) {
316-
this.config.onError?.(error!)
317-
this.config.onSettled?.(undefined, error!)
314+
} else if (action.type === 'Error' && result.isError) {
315+
config.onError?.(result.error!)
316+
config.onSettled?.(undefined, result.error!)
318317
this.updateTimers()
319318
}
320319

321-
this.updateListener?.(this.currentResult)
320+
// Decide if we need to notify the listener
321+
const notify =
322+
// Always notify on data or error change
323+
result.data !== prevResult.data ||
324+
result.error !== prevResult.error ||
325+
// Maybe notify on other changes
326+
config.notifyOnStatusChange
327+
328+
if (notify) {
329+
this.updateListener?.(result)
330+
}
322331
}
323332
}

src/core/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export interface QueryObserverConfig<
105105
* Defaults to `true`.
106106
*/
107107
refetchOnMount?: boolean
108+
/**
109+
* Whether a change to the query status should re-render a component.
110+
* If set to `false`, the component will only re-render when the actual `data` or `error` changes.
111+
* Defaults to `true`.
112+
*/
113+
notifyOnStatusChange?: boolean
108114
/**
109115
* This callback will fire any time the query successfully fetches new data.
110116
*/

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('useInfiniteQuery', () => {
6060
await waitFor(() => rendered.getByText('Status: success'))
6161

6262
expect(states[0]).toEqual({
63-
canFetchmore: undefined,
63+
canFetchMore: undefined,
6464
clear: expect.any(Function),
6565
data: undefined,
6666
error: null,

src/react/tests/useQuery.test.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
queryKey,
88
mockVisibilityState,
99
mockConsoleError,
10+
waitForMs,
1011
} from './utils'
1112
import { useQuery } from '..'
1213
import { queryCache, QueryResult } from '../../core'
@@ -518,7 +519,7 @@ describe('useQuery', () => {
518519

519520
await queryCache.prefetchQuery(key, () => 'prefetch')
520521

521-
await sleep(10)
522+
await sleep(40)
522523

523524
function FirstComponent() {
524525
const state = useQuery(key, () => 'one', {
@@ -530,7 +531,7 @@ describe('useQuery', () => {
530531

531532
function SecondComponent() {
532533
const state = useQuery(key, () => 'two', {
533-
staleTime: 5,
534+
staleTime: 20,
534535
})
535536
states2.push(state)
536537
return null
@@ -593,6 +594,76 @@ describe('useQuery', () => {
593594
})
594595
})
595596

597+
it('should re-render when a query becomes stale', async () => {
598+
const key = queryKey()
599+
const states: QueryResult<string>[] = []
600+
601+
function Page() {
602+
const state = useQuery(key, () => 'test', {
603+
staleTime: 50,
604+
})
605+
states.push(state)
606+
return null
607+
}
608+
609+
render(<Page />)
610+
611+
await waitFor(() => expect(states.length).toBe(3))
612+
613+
expect(states[0]).toMatchObject({
614+
isStale: true,
615+
})
616+
expect(states[1]).toMatchObject({
617+
isStale: false,
618+
})
619+
expect(states[2]).toMatchObject({
620+
isStale: true,
621+
})
622+
})
623+
624+
it('should not re-render when a query status changes and notifyOnStatusChange is false', async () => {
625+
const key = queryKey()
626+
const states: QueryResult<string>[] = []
627+
628+
function Page() {
629+
const state = useQuery(
630+
key,
631+
async () => {
632+
await sleep(5)
633+
return 'test'
634+
},
635+
{
636+
notifyOnStatusChange: false,
637+
}
638+
)
639+
640+
states.push(state)
641+
642+
const { refetch } = state
643+
644+
React.useEffect(() => {
645+
setTimeout(refetch, 10)
646+
}, [refetch])
647+
return null
648+
}
649+
650+
render(<Page />)
651+
652+
await waitForMs(30)
653+
654+
expect(states.length).toBe(2)
655+
expect(states[0]).toMatchObject({
656+
data: undefined,
657+
status: 'loading',
658+
isFetching: true,
659+
})
660+
expect(states[1]).toMatchObject({
661+
data: 'test',
662+
status: 'success',
663+
isFetching: false,
664+
})
665+
})
666+
596667
// See https://github.com/tannerlinsley/react-query/issues/137
597668
it('should not override initial data in dependent queries', async () => {
598669
const key1 = queryKey()

src/react/useMutation.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,6 @@ export function useMutation<
172172
if (mutateConfig.throwOnError ?? config.throwOnError) {
173173
throw error
174174
}
175-
176-
return
177175
}
178176
},
179177
[dispatch, getConfig, getMutationFn]

0 commit comments

Comments
 (0)