Skip to content

Commit 09dbf3c

Browse files
committed
feat: add notifyOnStatusChange flag
1 parent f3b2fe0 commit 09dbf3c

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 ||||
@@ -32,6 +31,8 @@ Feature/Capability Key:
3231
| Initial Data ||||
3332
| Scroll Recovery ||||
3433
| Cache Manipulation ||||
34+
| Stale While Revalidate ||| 🛑 |
35+
| Stale Time Configuration || 🛑 | 🛑 |
3536
| Outdated Query Dismissal ||||
3637
| Auto Garbage Collection || 🛑 | 🛑 |
3738
| Mutation Hooks || 🟡 ||

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
@@ -409,7 +409,7 @@ export class Query<TResult, TError> {
409409
const config = this.config
410410

411411
// Check if there is a query function
412-
if (!config.queryFn) {
412+
if (typeof config.queryFn !== 'function') {
413413
return
414414
}
415415

src/core/queryCache.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,6 @@ export class QueryCache {
313313
if (options?.throwOnError) {
314314
throw error
315315
}
316-
return
317316
}
318317
}
319318

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,
@@ -303,20 +297,35 @@ export class QueryObserver<TResult, TError> {
303297
_state: QueryState<TResult, TError>,
304298
action: Action<TResult, TError>
305299
): void {
306-
this.currentResult = this.createResult()
300+
const { config } = this
307301

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

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

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

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'
@@ -468,7 +469,7 @@ describe('useQuery', () => {
468469

469470
await queryCache.prefetchQuery(key, () => 'prefetch')
470471

471-
await sleep(10)
472+
await sleep(40)
472473

473474
function FirstComponent() {
474475
const state = useQuery(key, () => 'one', {
@@ -480,7 +481,7 @@ describe('useQuery', () => {
480481

481482
function SecondComponent() {
482483
const state = useQuery(key, () => 'two', {
483-
staleTime: 5,
484+
staleTime: 20,
484485
})
485486
states2.push(state)
486487
return null
@@ -543,6 +544,76 @@ describe('useQuery', () => {
543544
})
544545
})
545546

547+
it('should re-render when a query becomes stale', async () => {
548+
const key = queryKey()
549+
const states: QueryResult<string>[] = []
550+
551+
function Page() {
552+
const state = useQuery(key, () => 'test', {
553+
staleTime: 50,
554+
})
555+
states.push(state)
556+
return null
557+
}
558+
559+
render(<Page />)
560+
561+
await waitFor(() => expect(states.length).toBe(3))
562+
563+
expect(states[0]).toMatchObject({
564+
isStale: true,
565+
})
566+
expect(states[1]).toMatchObject({
567+
isStale: false,
568+
})
569+
expect(states[2]).toMatchObject({
570+
isStale: true,
571+
})
572+
})
573+
574+
it('should not re-render when a query status changes and notifyOnStatusChange is false', async () => {
575+
const key = queryKey()
576+
const states: QueryResult<string>[] = []
577+
578+
function Page() {
579+
const state = useQuery(
580+
key,
581+
async () => {
582+
await sleep(5)
583+
return 'test'
584+
},
585+
{
586+
notifyOnStatusChange: false,
587+
}
588+
)
589+
590+
states.push(state)
591+
592+
const { refetch } = state
593+
594+
React.useEffect(() => {
595+
setTimeout(refetch, 10)
596+
}, [refetch])
597+
return null
598+
}
599+
600+
render(<Page />)
601+
602+
await waitForMs(30)
603+
604+
expect(states.length).toBe(2)
605+
expect(states[0]).toMatchObject({
606+
data: undefined,
607+
status: 'loading',
608+
isFetching: true,
609+
})
610+
expect(states[1]).toMatchObject({
611+
data: 'test',
612+
status: 'success',
613+
isFetching: false,
614+
})
615+
})
616+
546617
// See https://github.com/tannerlinsley/react-query/issues/137
547618
it('should not override initial data in dependent queries', async () => {
548619
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)