Skip to content

Commit 99951a0

Browse files
committed
feat: add granular component re-render control
1 parent d01f52d commit 99951a0

File tree

6 files changed

+64
-48
lines changed

6 files changed

+64
-48
lines changed

docs/src/pages/reference/useQuery.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ const {
2626
initialData,
2727
isDataEqual,
2828
keepPreviousData,
29-
notifyOnFetchChange,
30-
notifyOnStaleChange,
31-
notifyOnStatusChange,
29+
notifyOnChangeProps,
30+
notifyOnChangePropsExclusions,
3231
onError,
3332
onSettled,
3433
onSuccess,
@@ -109,14 +108,14 @@ const result = useQuery({
109108
- If set to `true`, the query will refetch on reconnect if the data is stale.
110109
- If set to `false`, the query will not refetch on reconnect.
111110
- If set to `"always"`, the query will always refetch on reconnect.
112-
- `notifyOnStaleChange: boolean`
111+
- `notifyOnChangeProps: string[]`
113112
- Optional
114-
- Set this to `true` to re-render when the `isStale` property changes.
115-
- Defaults to `false`.
116-
- `notifyOnStatusChange: boolean`
113+
- If set, the component will only re-render if any of the listed properties change.
114+
- If set to `['data', 'error']` for example, the component will only re-render when the `data` or `error` properties change.
115+
- `notifyOnChangePropsExclusions: string[]`
117116
- Optional
118-
- Set this to `false` to only re-render when there are changes to `data` or `error`.
119-
- Defaults to `true`.
117+
- If set, the component will not re-render if any of the listed properties change.
118+
- If set to `['isStale']` for example, the component will not re-render when the `isStale` property changes.
120119
- `onSuccess: (data: TData) => void`
121120
- Optional
122121
- This function will fire any time the query successfully fetches new data.

src/core/queryObserver.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,10 @@ export class QueryObserver<
288288

289289
this.staleTimeoutId = setTimeout(() => {
290290
if (!this.currentResult.isStale) {
291+
const prevResult = this.currentResult
291292
this.updateResult()
292293
this.notify({
293-
listeners: this.options.notifyOnStaleChange === true,
294+
listeners: this.shouldNotifyListeners(prevResult, this.currentResult),
294295
cache: true,
295296
})
296297
}
@@ -423,6 +424,42 @@ export class QueryObserver<
423424
return result as QueryObserverResult<TData, TError>
424425
}
425426

427+
private shouldNotifyListeners(
428+
prevResult: QueryObserverResult,
429+
result: QueryObserverResult
430+
): boolean {
431+
const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options
432+
433+
if (prevResult === result) {
434+
return false
435+
}
436+
437+
if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {
438+
return true
439+
}
440+
441+
const keys = Object.keys(result)
442+
443+
for (let i = 0; i < keys.length; i++) {
444+
const key = keys[i] as keyof QueryObserverResult
445+
const changed = prevResult[key] !== result[key]
446+
const isIncluded = notifyOnChangeProps?.some(x => x === key)
447+
const isExcluded = notifyOnChangePropsExclusions?.some(x => x === key)
448+
449+
if (changed) {
450+
if (notifyOnChangePropsExclusions && isExcluded) {
451+
break
452+
}
453+
454+
if (!notifyOnChangeProps || isIncluded) {
455+
return true
456+
}
457+
}
458+
}
459+
460+
return false
461+
}
462+
426463
private updateResult(willFetch?: boolean): void {
427464
const result = this.getNewResult(willFetch)
428465

@@ -466,7 +503,9 @@ export class QueryObserver<
466503
prevQuery?.removeObserver(this)
467504
this.currentQuery.addObserver(this)
468505

469-
if (this.options.notifyOnStatusChange !== false) {
506+
if (
507+
this.shouldNotifyListeners(this.previousQueryResult, this.currentResult)
508+
) {
470509
this.notify({ listeners: true })
471510
}
472511
}
@@ -494,13 +533,7 @@ export class QueryObserver<
494533
notifyOptions.onError = true
495534
}
496535

497-
if (
498-
// Always notify if notifyOnStatusChange is set
499-
this.options.notifyOnStatusChange !== false ||
500-
// Otherwise only notify on data or error change
501-
currentResult.data !== prevResult.data ||
502-
currentResult.error !== prevResult.error
503-
) {
536+
if (this.shouldNotifyListeners(prevResult, currentResult)) {
504537
notifyOptions.listeners = true
505538
}
506539

src/core/tests/queryCache.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ describe('queryCache', () => {
614614
const testCache = new QueryCache()
615615
const testClient = new QueryClient({
616616
queryCache: testCache,
617-
defaultOptions: { queries: { notifyOnStaleChange: true } },
617+
defaultOptions: { queries: { notifyOnChangePropsExclusions: [] } },
618618
})
619619
const observer = new QueryObserver(testClient, {
620620
queryKey: key,

src/core/types.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,14 @@ export interface QueryObserverOptions<
128128
*/
129129
refetchOnMount?: boolean | 'always'
130130
/**
131-
* Whether a component should re-render when the `isStale` property changes.
132-
* Defaults to `false`.
131+
* If set, the component will only re-render if any of the listed properties change.
132+
* When set to `['data', 'error']`, the component will only re-render when the `data` or `error` properties change.
133133
*/
134-
notifyOnStaleChange?: boolean
134+
notifyOnChangeProps?: Array<keyof InfiniteQueryObserverResult>
135135
/**
136-
* Whether a change to the query status should re-render a component.
137-
* If set to `false`, the component will only re-render when the actual `data` or `error` changes.
138-
* Defaults to `true`.
136+
* If set, the component will not re-render if any of the listed properties change.
139137
*/
140-
notifyOnStatusChange?: boolean
138+
notifyOnChangePropsExclusions?: Array<keyof InfiniteQueryObserverResult>
141139
/**
142140
* This callback will fire any time the query successfully fetches new data.
143141
*/

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('useInfiniteQuery', () => {
4545
queryCache,
4646
defaultOptions: {
4747
queries: {
48-
notifyOnStaleChange: true,
48+
notifyOnChangePropsExclusions: [],
4949
},
5050
},
5151
})

src/react/tests/useQuery.test.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@ import {
2020

2121
describe('useQuery', () => {
2222
const queryCache = new QueryCache()
23-
const queryClient = new QueryClient({
24-
queryCache,
25-
defaultOptions: {
26-
queries: {
27-
notifyOnStaleChange: true,
28-
},
29-
},
30-
})
23+
const queryClient = new QueryClient({ queryCache })
3124

3225
it('should return the correct types', () => {
3326
const key = queryKey()
@@ -665,8 +658,7 @@ describe('useQuery', () => {
665658
function Page() {
666659
const state = useQuery(key, () => ({ name: 'test' }), {
667660
select: data => data.name,
668-
notifyOnStaleChange: false,
669-
notifyOnStatusChange: false,
661+
notifyOnChangeProps: ['data'],
670662
})
671663

672664
states.push(state)
@@ -1266,7 +1258,9 @@ describe('useQuery', () => {
12661258

12671259
renderWithClient(queryClient, <Page />)
12681260

1269-
await waitFor(() => expect(states.length).toBe(7))
1261+
await sleep(100)
1262+
1263+
expect(states.length).toBe(6)
12701264

12711265
// Disabled query
12721266
expect(states[0]).toMatchObject({
@@ -1298,20 +1292,13 @@ describe('useQuery', () => {
12981292
})
12991293
// Switched query key
13001294
expect(states[4]).toMatchObject({
1301-
data: 10,
1302-
isFetching: false,
1303-
isSuccess: true,
1304-
isPreviousData: true,
1305-
})
1306-
// Refetch
1307-
expect(states[5]).toMatchObject({
13081295
data: 10,
13091296
isFetching: true,
13101297
isSuccess: true,
13111298
isPreviousData: true,
13121299
})
13131300
// Refetch done
1314-
expect(states[6]).toMatchObject({
1301+
expect(states[5]).toMatchObject({
13151302
data: 12,
13161303
isFetching: false,
13171304
isSuccess: true,
@@ -1516,8 +1503,7 @@ describe('useQuery', () => {
15161503
return 'test'
15171504
},
15181505
{
1519-
notifyOnStatusChange: false,
1520-
notifyOnStaleChange: false,
1506+
notifyOnChangeProps: ['data'],
15211507
}
15221508
)
15231509

0 commit comments

Comments
 (0)