Skip to content

Commit 8766a35

Browse files
authored
fix: reduce the number of renders (#754)
1 parent b6b05d5 commit 8766a35

10 files changed

+410
-133
lines changed

src/core/query.js

+29-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
statusIdle,
1111
Console,
1212
getStatusBools,
13+
shallowEqual,
1314
} from './utils'
1415
import { makeQueryInstance } from './queryInstance'
1516

@@ -63,9 +64,14 @@ export function makeQuery({
6364
}
6465

6566
query.dispatch = action => {
66-
query.state = queryReducer(query.state, action)
67-
query.instances.forEach(d => d.onStateUpdate(query.state))
68-
notifyGlobalListeners(query)
67+
const newState = queryReducer(query.state, action)
68+
69+
// Only update state if something has changed
70+
if (!shallowEqual(query.state, newState)) {
71+
query.state = newState
72+
query.instances.forEach(d => d.onStateUpdate(query.state))
73+
notifyGlobalListeners(query)
74+
}
6975
}
7076

7177
query.scheduleStaleTimeout = () => {
@@ -143,11 +149,19 @@ export function makeQuery({
143149
query.setState = updater => query.dispatch({ type: actionSetState, updater })
144150

145151
query.setData = updater => {
152+
const isStale = query.config.staleTime === 0
153+
146154
// Set data and mark it as cached
147-
query.dispatch({ type: actionSuccess, updater })
155+
query.dispatch({
156+
type: actionSuccess,
157+
updater,
158+
isStale,
159+
})
148160

149-
// Schedule a fresh invalidation!
150-
query.scheduleStaleTimeout()
161+
if (!isStale) {
162+
// Schedule a fresh invalidation!
163+
query.scheduleStaleTimeout()
164+
}
151165
}
152166

153167
query.clear = () => {
@@ -172,11 +186,11 @@ export function makeQuery({
172186
const tryFetchData = async (fn, ...args) => {
173187
try {
174188
// Perform the query
175-
const promise = fn(...query.config.queryFnParamsFilter(args))
189+
const promiseOrValue = fn(...query.config.queryFnParamsFilter(args))
176190

177-
query.cancelPromises = () => promise.cancel?.()
191+
query.cancelPromises = () => promiseOrValue?.cancel?.()
178192

179-
const data = await promise
193+
const data = await promiseOrValue
180194
delete query.shouldContinueRetryOnFocus
181195

182196
delete query.cancelPromises
@@ -187,16 +201,16 @@ export function makeQuery({
187201
delete query.cancelPromises
188202
if (query.cancelled) throw query.cancelled
189203

190-
// If we fail, increase the failureCount
191-
query.dispatch({ type: actionFailed })
192-
193204
// Do we need to retry the request?
194205
if (
195206
query.config.retry === true ||
196-
query.state.failureCount <= query.config.retry ||
207+
query.state.failureCount < query.config.retry ||
197208
(typeof query.config.retry === 'function' &&
198209
query.config.retry(query.state.failureCount, error))
199210
) {
211+
// If we retry, increase the failureCount
212+
query.dispatch({ type: actionFailed })
213+
200214
// Only retry if the document is visible
201215
if (!isDocumentVisible()) {
202216
// set this flag to continue retries on focus
@@ -454,14 +468,15 @@ function switchActions(state, action) {
454468
status: statusSuccess,
455469
data: functionalUpdate(action.updater, state.data),
456470
error: null,
457-
isStale: false,
471+
isStale: action.isStale,
458472
isFetching: false,
459473
updatedAt: Date.now(),
460474
failureCount: 0,
461475
}
462476
case actionError:
463477
return {
464478
...state,
479+
failureCount: state.failureCount + 1,
465480
isFetching: false,
466481
isStale: true,
467482
...(!action.cancelled && {

src/core/tests/queryCache.test.js

+23-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('queryCache', () => {
4747
})
4848

4949
test('prefetchQuery should not force fetch', async () => {
50-
queryCache.setQueryData('key', 'og')
50+
queryCache.setQueryData('key', 'og', { staleTime: 100 })
5151
const fetchFn = () => Promise.resolve('new')
5252
const first = await queryCache.prefetchQuery(
5353
'key',
@@ -77,24 +77,28 @@ describe('queryCache', () => {
7777
).rejects.toEqual(new Error('error'))
7878
})
7979

80-
test('should notify listeners when new query is added', () => {
80+
test('should notify listeners when new query is added', async () => {
8181
const callback = jest.fn()
8282

8383
queryCache.subscribe(callback)
8484

8585
queryCache.prefetchQuery('test', () => 'data')
8686

87+
await sleep(100)
88+
8789
expect(callback).toHaveBeenCalled()
8890
})
8991

90-
test('should include the queryCache and query when notifying listeners', () => {
92+
test('should include the queryCache and query when notifying listeners', async () => {
9193
const callback = jest.fn()
9294

9395
queryCache.subscribe(callback)
9496

9597
queryCache.prefetchQuery('test', () => 'data')
9698
const query = queryCache.getQuery('test')
9799

100+
await sleep(100)
101+
98102
expect(callback).toHaveBeenCalledWith(queryCache, query)
99103
})
100104

@@ -136,6 +140,21 @@ describe('queryCache', () => {
136140
expect(queryCache.getQuery('key')).toBeFalsy()
137141
})
138142

143+
test('setQueryData should schedule stale timeout, if staleTime is set', async () => {
144+
queryCache.setQueryData('key', 'test data', { staleTime: 10 })
145+
expect(queryCache.getQuery('key').staleTimeout).not.toBeUndefined()
146+
})
147+
148+
test('setQueryData should not schedule stale timeout by default', async () => {
149+
queryCache.setQueryData('key', 'test data')
150+
expect(queryCache.getQuery('key').staleTimeout).toBeUndefined()
151+
})
152+
153+
test('setQueryData should not schedule stale timeout, if staleTime is set to `Infinity`', async () => {
154+
queryCache.setQueryData('key', 'test data', { staleTime: Infinity })
155+
expect(queryCache.getQuery('key').staleTimeout).toBeUndefined()
156+
})
157+
139158
test('setQueryData schedules stale timeouts appropriately', async () => {
140159
queryCache.setQueryData('key', 'test data', { staleTime: 100 })
141160

@@ -174,7 +193,7 @@ describe('queryCache', () => {
174193
test('stale timeout dispatch is not called if query is no longer in the query cache', async () => {
175194
const queryKey = 'key'
176195
const fetchData = () => Promise.resolve('data')
177-
await queryCache.prefetchQuery(queryKey, fetchData)
196+
await queryCache.prefetchQuery(queryKey, fetchData, { staleTime: 100 })
178197
const query = queryCache.getQuery(queryKey)
179198
expect(query.state.isStale).toBe(false)
180199
queryCache.removeQueries(queryKey)

src/core/tests/utils.test.js

+68-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { setConsole, queryCache, queryCaches } from '../'
2-
import { deepEqual } from '../utils'
2+
import { deepEqual, shallowEqual } from '../utils'
33

44
describe('core/utils', () => {
55
afterEach(() => {
@@ -28,17 +28,76 @@ describe('core/utils', () => {
2828
setConsole(console)
2929
})
3030

31-
it('deepequal should return `false` for different dates', () => {
32-
const date1 = new Date(2020, 3, 1)
33-
const date2 = new Date(2020, 3, 2)
31+
describe('deepEqual', () => {
32+
it('should return `true` for equal objects', () => {
33+
const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
34+
const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
35+
expect(deepEqual(a, b)).toEqual(true)
36+
})
3437

35-
expect(deepEqual(date1, date2)).toEqual(false)
38+
it('should return `false` for non equal objects', () => {
39+
const a = { a: { b: 'b' }, c: 'c' }
40+
const b = { a: { b: 'c' }, c: 'c' }
41+
expect(deepEqual(a, b)).toEqual(false)
42+
})
43+
44+
it('should return `false` for different dates', () => {
45+
const date1 = new Date(2020, 3, 1)
46+
const date2 = new Date(2020, 3, 2)
47+
expect(deepEqual(date1, date2)).toEqual(false)
48+
})
49+
50+
it('return `true` for equal dates', () => {
51+
const date1 = new Date(2020, 3, 1)
52+
const date2 = new Date(2020, 3, 1)
53+
expect(deepEqual(date1, date2)).toEqual(true)
54+
})
3655
})
3756

38-
it('should return `true` for equal dates', () => {
39-
const date1 = new Date(2020, 3, 1)
40-
const date2 = new Date(2020, 3, 1)
57+
describe('shallowEqual', () => {
58+
it('should return `true` for empty objects', () => {
59+
expect(shallowEqual({}, {})).toEqual(true)
60+
})
61+
62+
it('should return `true` for equal values', () => {
63+
expect(shallowEqual(1, 1)).toEqual(true)
64+
})
65+
66+
it('should return `true` for equal arrays', () => {
67+
expect(shallowEqual([1, 2], [1, 2])).toEqual(true)
68+
})
69+
70+
it('should return `true` for equal shallow objects', () => {
71+
const a = { a: 'a', b: 'b' }
72+
const b = { a: 'a', b: 'b' }
73+
expect(shallowEqual(a, b)).toEqual(true)
74+
})
75+
76+
it('should return `true` for equal deep objects with same identities', () => {
77+
const deep = { b: 'b' }
78+
const a = { a: deep, c: 'c' }
79+
const b = { a: deep, c: 'c' }
80+
expect(shallowEqual(a, b)).toEqual(true)
81+
})
82+
83+
it('should return `false` for non equal values', () => {
84+
expect(shallowEqual(1, 2)).toEqual(false)
85+
})
86+
87+
it('should return `false` for equal arrays', () => {
88+
expect(shallowEqual([1, 2], [1, 3])).toEqual(false)
89+
})
90+
91+
it('should return `false` for non equal shallow objects', () => {
92+
const a = { a: 'a', b: 'b' }
93+
const b = { a: 'a', b: 'c' }
94+
expect(shallowEqual(a, b)).toEqual(false)
95+
})
4196

42-
expect(deepEqual(date1, date2)).toEqual(true)
97+
it('should return `false` for equal deep objects with different identities', () => {
98+
const a = { a: { b: 'b' }, c: 'c' }
99+
const b = { a: { b: 'b' }, c: 'c' }
100+
expect(shallowEqual(a, b)).toEqual(false)
101+
})
43102
})
44103
})

src/core/utils.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,33 @@ export function getQueryArgs(args) {
9090
return [queryKey, queryFn ? { ...config, queryFn } : config, ...rest]
9191
}
9292

93+
export function deepEqual(a, b) {
94+
return equal(a, b, true)
95+
}
96+
97+
export function shallowEqual(a, b) {
98+
return equal(a, b, false)
99+
}
100+
93101
// This deep-equal is directly based on https://github.com/epoberezkin/fast-deep-equal.
94102
// The parts for comparing any non-JSON-supported values has been removed
95-
export function deepEqual(a, b) {
103+
function equal(a, b, deep, depth = 0) {
96104
if (a === b) return true
97105

98-
if (a && b && typeof a == 'object' && typeof b == 'object') {
106+
if (
107+
(deep || !depth) &&
108+
a &&
109+
b &&
110+
typeof a == 'object' &&
111+
typeof b == 'object'
112+
) {
99113
var length, i, keys
100114
if (Array.isArray(a)) {
101115
length = a.length
102116
// eslint-disable-next-line eqeqeq
103117
if (length != b.length) return false
104-
for (i = length; i-- !== 0; ) if (!deepEqual(a[i], b[i])) return false
118+
for (i = length; i-- !== 0; )
119+
if (!equal(a[i], b[i], deep, depth + 1)) return false
105120
return true
106121
}
107122

@@ -118,7 +133,7 @@ export function deepEqual(a, b) {
118133
for (i = length; i-- !== 0; ) {
119134
var key = keys[i]
120135

121-
if (!deepEqual(a[key], b[key])) return false
136+
if (!equal(a[key], b[key], deep, depth + 1)) return false
122137
}
123138

124139
return true

src/react/tests/useInfiniteQuery.test.js

+66
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,72 @@ describe('useInfiniteQuery', () => {
2828
queryCaches.forEach(cache => cache.clear({ notify: false }))
2929
})
3030

31+
it('should return the correct states for a successful query', async () => {
32+
let count = 0
33+
const states = []
34+
35+
function Page() {
36+
const state = useInfiniteQuery(
37+
'items',
38+
(key, nextId = 0) => fetchItems(nextId, count++),
39+
{
40+
getFetchMore: (lastGroup, allGroups) => Boolean(lastGroup.nextId),
41+
}
42+
)
43+
44+
states.push(state)
45+
46+
return (
47+
<div>
48+
<h1>Status: {state.status}</h1>
49+
</div>
50+
)
51+
}
52+
53+
const rendered = render(<Page />)
54+
55+
await waitFor(() => rendered.getByText('Status: success'))
56+
57+
expect(states[0]).toMatchObject({
58+
clear: expect.any(Function),
59+
data: undefined,
60+
error: null,
61+
failureCount: 0,
62+
fetchMore: expect.any(Function),
63+
isError: false,
64+
isFetching: true,
65+
isIdle: false,
66+
isLoading: true,
67+
isStale: true,
68+
isSuccess: false,
69+
refetch: expect.any(Function),
70+
status: 'loading',
71+
})
72+
73+
expect(states[1]).toMatchObject({
74+
clear: expect.any(Function),
75+
canFetchMore: true,
76+
data: [
77+
{
78+
items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
79+
nextId: 1,
80+
ts: 0,
81+
},
82+
],
83+
error: null,
84+
failureCount: 0,
85+
fetchMore: expect.any(Function),
86+
isError: false,
87+
isFetching: false,
88+
isIdle: false,
89+
isLoading: false,
90+
isStale: true,
91+
isSuccess: true,
92+
refetch: expect.any(Function),
93+
status: 'success',
94+
})
95+
})
96+
3197
it('should allow you to fetch more pages', async () => {
3298
function Page() {
3399
const fetchCountRef = React.useRef(0)

0 commit comments

Comments
 (0)