Skip to content

Commit bffdc0e

Browse files
authored
fix: prevent redundant renders (#1487)
1 parent e67b9e8 commit bffdc0e

File tree

5 files changed

+85
-73
lines changed

5 files changed

+85
-73
lines changed

src/core/queryObserver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ export class QueryObserver<
409409
data,
410410
dataUpdatedAt,
411411
error: state.error,
412-
errorUpdatedAt: state.errorUpdateCount,
412+
errorUpdatedAt: state.errorUpdatedAt,
413413
failureCount: state.fetchFailureCount,
414414
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
415415
isFetchedAfterMount:

src/react/tests/suspense.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,62 @@ import {
99
QueryCache,
1010
QueryErrorResetBoundary,
1111
useQueryErrorResetBoundary,
12+
UseQueryResult,
1213
} from '../..'
1314

1415
describe("useQuery's in Suspense mode", () => {
1516
const queryCache = new QueryCache()
1617
const queryClient = new QueryClient({ queryCache })
1718

19+
it('should render the correct amount of times in Suspense mode', async () => {
20+
const key = queryKey()
21+
const states: UseQueryResult<number>[] = []
22+
23+
let count = 0
24+
let renders = 0
25+
26+
function Page() {
27+
renders++
28+
29+
const [stateKey, setStateKey] = React.useState(key)
30+
31+
const state = useQuery(
32+
stateKey,
33+
async () => {
34+
count++
35+
await sleep(10)
36+
return count
37+
},
38+
{ suspense: true }
39+
)
40+
41+
states.push(state)
42+
43+
return (
44+
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
45+
)
46+
}
47+
48+
const rendered = renderWithClient(
49+
queryClient,
50+
<React.Suspense fallback="loading">
51+
<Page />
52+
</React.Suspense>
53+
)
54+
55+
await sleep(20)
56+
57+
await waitFor(() => rendered.getByLabelText('toggle'))
58+
fireEvent.click(rendered.getByLabelText('toggle'))
59+
60+
await sleep(20)
61+
62+
expect(renders).toBe(4)
63+
expect(states.length).toBe(2)
64+
expect(states[0]).toMatchObject({ data: 1, status: 'success' })
65+
expect(states[1]).toMatchObject({ data: 2, status: 'success' })
66+
})
67+
1868
it('should not call the queryFn twice when used in Suspense mode', async () => {
1969
const key = queryKey()
2070

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe('useInfiniteQuery', () => {
206206

207207
await sleep(300)
208208

209-
expect(states.length).toBe(7)
209+
expect(states.length).toBe(6)
210210
expect(states[0]).toMatchObject({
211211
data: undefined,
212212
isFetching: true,
@@ -244,13 +244,6 @@ describe('useInfiniteQuery', () => {
244244
isPreviousData: true,
245245
})
246246
expect(states[5]).toMatchObject({
247-
data: { pages: ['0-desc', '1-desc'] },
248-
isFetching: true,
249-
isFetchingNextPage: false,
250-
isSuccess: true,
251-
isPreviousData: true,
252-
})
253-
expect(states[6]).toMatchObject({
254247
data: { pages: ['0-asc'] },
255248
isFetching: false,
256249
isFetchingNextPage: false,
@@ -823,7 +816,7 @@ describe('useInfiniteQuery', () => {
823816

824817
await sleep(100)
825818

826-
expect(states.length).toBe(6)
819+
expect(states.length).toBe(5)
827820
expect(states[0]).toMatchObject({
828821
hasNextPage: undefined,
829822
data: undefined,
@@ -847,24 +840,16 @@ describe('useInfiniteQuery', () => {
847840
isFetchingNextPage: false,
848841
isSuccess: true,
849842
})
850-
// Cache update
851-
expect(states[3]).toMatchObject({
852-
hasNextPage: true,
853-
data: { pages: [7, 8] },
854-
isFetching: false,
855-
isFetchingNextPage: false,
856-
isSuccess: true,
857-
})
858843
// Refetch
859-
expect(states[4]).toMatchObject({
844+
expect(states[3]).toMatchObject({
860845
hasNextPage: true,
861846
data: { pages: [7, 8] },
862847
isFetching: true,
863848
isFetchingNextPage: false,
864849
isSuccess: true,
865850
})
866851
// Refetch done
867-
expect(states[5]).toMatchObject({
852+
expect(states[4]).toMatchObject({
868853
hasNextPage: true,
869854
data: { pages: [7, 8] },
870855
isFetching: false,

src/react/tests/useQuery.test.tsx

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -568,17 +568,15 @@ describe('useQuery', () => {
568568

569569
await sleep(100)
570570

571-
expect(states.length).toBe(5)
571+
expect(states.length).toBe(4)
572572
// First load
573573
expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false })
574574
// First success
575575
expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true })
576-
// Switch
577-
expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false })
578576
// Second load
579-
expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false })
577+
expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false })
580578
// Second success
581-
expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true })
579+
expect(states[3]).toMatchObject({ isLoading: false, isSuccess: true })
582580
})
583581

584582
it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => {
@@ -768,17 +766,15 @@ describe('useQuery', () => {
768766

769767
await sleep(20)
770768

771-
expect(states.length).toBe(5)
769+
expect(states.length).toBe(4)
772770
// Initial
773771
expect(states[0]).toMatchObject({ data: undefined })
774772
// Fetched
775773
expect(states[1]).toMatchObject({ data: 1 })
776-
// Rerender
777-
expect(states[2]).toMatchObject({ data: undefined })
778774
// Switch
779-
expect(states[3]).toMatchObject({ data: undefined })
775+
expect(states[2]).toMatchObject({ data: undefined })
780776
// Fetched
781-
expect(states[4]).toMatchObject({ data: 2 })
777+
expect(states[3]).toMatchObject({ data: 2 })
782778
})
783779

784780
it('should be create a new query when refetching a removed query', async () => {
@@ -1084,7 +1080,7 @@ describe('useQuery', () => {
10841080

10851081
renderWithClient(queryClient, <Page />)
10861082

1087-
await waitFor(() => expect(states.length).toBe(5))
1083+
await waitFor(() => expect(states.length).toBe(4))
10881084

10891085
// Initial
10901086
expect(states[0]).toMatchObject({
@@ -1107,15 +1103,8 @@ describe('useQuery', () => {
11071103
isSuccess: true,
11081104
isPreviousData: true,
11091105
})
1110-
// Previous data
1111-
expect(states[3]).toMatchObject({
1112-
data: 0,
1113-
isFetching: true,
1114-
isSuccess: true,
1115-
isPreviousData: true,
1116-
})
11171106
// New data
1118-
expect(states[4]).toMatchObject({
1107+
expect(states[3]).toMatchObject({
11191108
data: 1,
11201109
isFetching: false,
11211110
isSuccess: true,
@@ -1152,7 +1141,7 @@ describe('useQuery', () => {
11521141

11531142
renderWithClient(queryClient, <Page />)
11541143

1155-
await waitFor(() => expect(states.length).toBe(5))
1144+
await waitFor(() => expect(states.length).toBe(4))
11561145

11571146
// Initial
11581147
expect(states[0]).toMatchObject({
@@ -1175,15 +1164,8 @@ describe('useQuery', () => {
11751164
isSuccess: true,
11761165
isPreviousData: true,
11771166
})
1178-
// Previous data
1179-
expect(states[3]).toMatchObject({
1180-
data: 0,
1181-
isFetching: true,
1182-
isSuccess: true,
1183-
isPreviousData: true,
1184-
})
11851167
// New data
1186-
expect(states[4]).toMatchObject({
1168+
expect(states[3]).toMatchObject({
11871169
data: 1,
11881170
isFetching: false,
11891171
isSuccess: true,
@@ -1228,7 +1210,7 @@ describe('useQuery', () => {
12281210

12291211
renderWithClient(queryClient, <Page />)
12301212

1231-
await waitFor(() => expect(states.length).toBe(7))
1213+
await waitFor(() => expect(states.length).toBe(6))
12321214

12331215
// Disabled query
12341216
expect(states[0]).toMatchObject({
@@ -1258,22 +1240,15 @@ describe('useQuery', () => {
12581240
isSuccess: true,
12591241
isPreviousData: true,
12601242
})
1261-
// Switched query key
1262-
expect(states[4]).toMatchObject({
1263-
data: 0,
1264-
isFetching: false,
1265-
isSuccess: true,
1266-
isPreviousData: true,
1267-
})
12681243
// Fetching new query
1269-
expect(states[5]).toMatchObject({
1244+
expect(states[4]).toMatchObject({
12701245
data: 0,
12711246
isFetching: true,
12721247
isSuccess: true,
12731248
isPreviousData: true,
12741249
})
12751250
// Fetched new query
1276-
expect(states[6]).toMatchObject({
1251+
expect(states[5]).toMatchObject({
12771252
data: 1,
12781253
isFetching: false,
12791254
isSuccess: true,
@@ -1324,7 +1299,7 @@ describe('useQuery', () => {
13241299

13251300
await sleep(100)
13261301

1327-
expect(states.length).toBe(6)
1302+
expect(states.length).toBe(5)
13281303

13291304
// Disabled query
13301305
expect(states[0]).toMatchObject({
@@ -1340,29 +1315,22 @@ describe('useQuery', () => {
13401315
isSuccess: true,
13411316
isPreviousData: true,
13421317
})
1343-
// Switched query key
1344-
expect(states[2]).toMatchObject({
1345-
data: 10,
1346-
isFetching: false,
1347-
isSuccess: true,
1348-
isPreviousData: true,
1349-
})
13501318
// Set state
1351-
expect(states[3]).toMatchObject({
1319+
expect(states[2]).toMatchObject({
13521320
data: 10,
13531321
isFetching: false,
13541322
isSuccess: true,
13551323
isPreviousData: true,
13561324
})
13571325
// Switched query key
1358-
expect(states[4]).toMatchObject({
1326+
expect(states[3]).toMatchObject({
13591327
data: 10,
13601328
isFetching: true,
13611329
isSuccess: true,
13621330
isPreviousData: true,
13631331
})
13641332
// Refetch done
1365-
expect(states[5]).toMatchObject({
1333+
expect(states[4]).toMatchObject({
13661334
data: 12,
13671335
isFetching: false,
13681336
isSuccess: true,
@@ -2126,13 +2094,11 @@ describe('useQuery', () => {
21262094

21272095
await sleep(100)
21282096

2129-
expect(states.length).toBe(3)
2097+
expect(states.length).toBe(2)
21302098
// Initial
21312099
expect(states[0]).toMatchObject({ data: { count: 0 } })
21322100
// Set state
21332101
expect(states[1]).toMatchObject({ data: { count: 1 } })
2134-
// Update
2135-
expect(states[2]).toMatchObject({ data: { count: 1 } })
21362102
})
21372103

21382104
it('should retry specified number of times', async () => {

src/react/useBaseQuery.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22

33
import { notifyManager } from '../core/notifyManager'
44
import { QueryObserver } from '../core/queryObserver'
5+
import { QueryObserverResult } from '../core/types'
56
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
67
import { useQueryClient } from './QueryClientProvider'
78
import { UseBaseQueryOptions } from './types'
@@ -56,10 +57,20 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
5657
const [, rerender] = React.useState({})
5758
const currentResult = observer.getCurrentResult()
5859

60+
// Remember latest result to prevent redundant renders
61+
const latestResultRef = React.useRef(currentResult)
62+
latestResultRef.current = currentResult
63+
5964
// Subscribe to the observer
6065
React.useEffect(() => {
6166
errorResetBoundary.clearReset()
62-
return observer.subscribe(notifyManager.batchCalls(rerender))
67+
return observer.subscribe(
68+
notifyManager.batchCalls((result: QueryObserverResult) => {
69+
if (result !== latestResultRef.current) {
70+
rerender({})
71+
}
72+
})
73+
)
6374
}, [observer, errorResetBoundary])
6475

6576
// Handle suspense

0 commit comments

Comments
 (0)