Skip to content

Commit 460ab33

Browse files
authored
fix: make sure initial data always uses initial stale (#1010)
1 parent 380a049 commit 460ab33

File tree

2 files changed

+89
-51
lines changed

2 files changed

+89
-51
lines changed

src/core/queryObserver.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
isValidTimeout,
66
noop,
77
} from './utils'
8-
import { QueryResult, ResolvedQueryConfig, QueryStatus } from './types'
8+
import type { QueryResult, ResolvedQueryConfig } from './types'
99
import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query'
1010

1111
export type UpdateListener<TResult, TError> = (
@@ -19,12 +19,14 @@ export class QueryObserver<TResult, TError> {
1919
private currentResult!: QueryResult<TResult, TError>
2020
private previousQueryResult?: QueryResult<TResult, TError>
2121
private listener?: UpdateListener<TResult, TError>
22+
private isStale: boolean
2223
private initialUpdateCount: number
2324
private staleTimeoutId?: number
2425
private refetchIntervalId?: number
2526

2627
constructor(config: ResolvedQueryConfig<TResult, TError>) {
2728
this.config = config
29+
this.isStale = true
2830
this.initialUpdateCount = 0
2931

3032
// Bind exposed methods
@@ -128,9 +130,9 @@ export class QueryObserver<TResult, TError> {
128130

129131
private optionalFetch(): void {
130132
if (
131-
this.config.enabled && // Don't auto refetch if disabled
133+
this.config.enabled && // Only fetch if enabled
134+
this.isStale && // Only fetch if stale
132135
!(this.config.suspense && this.currentResult.isFetched) && // Don't refetch if in suspense mode and the data is already fetched
133-
this.currentResult.isStale && // Only refetch if stale
134136
(this.config.refetchOnMount || this.currentQuery.observers.length === 1)
135137
) {
136138
this.fetch()
@@ -148,7 +150,7 @@ export class QueryObserver<TResult, TError> {
148150

149151
this.clearStaleTimeout()
150152

151-
if (this.currentResult.isStale || !isValidTimeout(this.config.staleTime)) {
153+
if (this.isStale || !isValidTimeout(this.config.staleTime)) {
152154
return
153155
}
154156

@@ -157,8 +159,9 @@ export class QueryObserver<TResult, TError> {
157159
const timeout = Math.max(timeUntilStale, 0)
158160

159161
this.staleTimeoutId = setTimeout(() => {
160-
if (!this.currentResult.isStale) {
161-
this.currentResult = { ...this.currentResult, isStale: true }
162+
if (!this.isStale) {
163+
this.isStale = true
164+
this.updateResult()
162165
this.notify()
163166
this.config.queryCache.notifyGlobalListeners(this.currentQuery)
164167
}
@@ -208,40 +211,23 @@ export class QueryObserver<TResult, TError> {
208211
}
209212

210213
private updateResult(): void {
211-
const { currentQuery, currentResult, previousQueryResult, config } = this
214+
const { currentQuery, previousQueryResult, config } = this
212215
const { state } = currentQuery
213216
let { data, status, updatedAt } = state
214217
let isPreviousData = false
215218

216219
// Keep previous data if needed
217220
if (
218221
config.keepPreviousData &&
219-
(state.status === QueryStatus.Idle ||
220-
state.status === QueryStatus.Loading) &&
221-
previousQueryResult?.status === QueryStatus.Success
222+
state.isInitialData &&
223+
previousQueryResult?.isSuccess
222224
) {
223225
data = previousQueryResult.data
224226
updatedAt = previousQueryResult.updatedAt
225227
status = previousQueryResult.status
226228
isPreviousData = true
227229
}
228230

229-
let isStale
230-
231-
// When the query has not been fetched yet and this is the initial render,
232-
// determine the staleness based on the initialStale or existence of initial data.
233-
if (!currentResult && state.isInitialData) {
234-
if (typeof config.initialStale === 'function') {
235-
isStale = config.initialStale()
236-
} else if (typeof config.initialStale === 'boolean') {
237-
isStale = config.initialStale
238-
} else {
239-
isStale = typeof state.data === 'undefined'
240-
}
241-
} else {
242-
isStale = currentQuery.isStaleByTime(config.staleTime)
243-
}
244-
245231
this.currentResult = {
246232
...getStatusProps(status),
247233
canFetchMore: state.canFetchMore,
@@ -256,22 +242,16 @@ export class QueryObserver<TResult, TError> {
256242
isFetchingMore: state.isFetchingMore,
257243
isInitialData: state.isInitialData,
258244
isPreviousData,
259-
isStale,
245+
isStale: this.isStale,
260246
refetch: this.refetch,
261247
updatedAt,
262248
}
263249
}
264250

265251
private updateQuery(): void {
252+
const config = this.config
266253
const prevQuery = this.currentQuery
267254

268-
// Remove the initial data when there is an existing query
269-
// because this data should not be used for a new query
270-
const config =
271-
this.config.keepPreviousData && prevQuery
272-
? { ...this.config, initialData: undefined }
273-
: this.config
274-
275255
let query = config.queryCache.getQueryByHash<TResult, TError>(
276256
config.queryHash
277257
)
@@ -287,6 +267,22 @@ export class QueryObserver<TResult, TError> {
287267
this.previousQueryResult = this.currentResult
288268
this.currentQuery = query
289269
this.initialUpdateCount = query.state.updateCount
270+
271+
// Update stale state on query switch
272+
if (query.state.isInitialData) {
273+
if (config.keepPreviousData && prevQuery) {
274+
this.isStale = true
275+
} else if (typeof config.initialStale === 'function') {
276+
this.isStale = config.initialStale()
277+
} else if (typeof config.initialStale === 'boolean') {
278+
this.isStale = config.initialStale
279+
} else {
280+
this.isStale = typeof query.state.data === 'undefined'
281+
}
282+
} else {
283+
this.isStale = query.isStaleByTime(config.staleTime)
284+
}
285+
290286
this.updateResult()
291287

292288
if (this.listener) {
@@ -296,16 +292,20 @@ export class QueryObserver<TResult, TError> {
296292
}
297293

298294
onQueryUpdate(action: Action<TResult, TError>): void {
295+
const { config } = this
299296
const { type } = action
300297

298+
// Update stale state on success or error
299+
if (type === 2 || type === 3) {
300+
this.isStale = this.currentQuery.isStaleByTime(config.staleTime)
301+
}
302+
301303
// Store current result and get new result
302304
const prevResult = this.currentResult
303305
this.updateResult()
306+
const currentResult = this.currentResult
304307

305-
const { currentResult, config } = this
306-
307-
// We need to check the action because the state could have
308-
// transitioned from success to success in case of `setQueryData`.
308+
// Trigger callbacks and timers on success or error
309309
if (type === 2) {
310310
config.onSuccess?.(currentResult.data!)
311311
config.onSettled?.(currentResult.data!, null)

src/react/tests/useQuery.test.tsx

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,12 +1198,11 @@ describe('useQuery', () => {
11981198

11991199
render(<Page />)
12001200

1201-
await waitFor(() => expect(states.length).toBe(2))
1201+
await waitForMs(10)
12021202

1203-
expect(states).toMatchObject([
1204-
{ data: 'initial', isStale: false },
1205-
{ data: 'initial', isStale: true },
1206-
])
1203+
expect(states.length).toBe(2)
1204+
expect(states[0]).toMatchObject({ data: 'initial', isStale: false })
1205+
expect(states[1]).toMatchObject({ data: 'initial', isStale: true })
12071206
})
12081207

12091208
it('should fetch if initial data is set and initial stale is set to true', async () => {
@@ -1230,6 +1229,47 @@ describe('useQuery', () => {
12301229
])
12311230
})
12321231

1232+
it('should fetch if initial data is set and initial stale is set to true with stale time', async () => {
1233+
const key = queryKey()
1234+
const states: QueryResult<string>[] = []
1235+
1236+
function Page() {
1237+
const state = useQuery(key, () => 'data', {
1238+
staleTime: 50,
1239+
initialData: 'initial',
1240+
initialStale: true,
1241+
})
1242+
states.push(state)
1243+
return null
1244+
}
1245+
1246+
render(<Page />)
1247+
1248+
await waitForMs(100)
1249+
1250+
expect(states.length).toBe(4)
1251+
expect(states[0]).toMatchObject({
1252+
data: 'initial',
1253+
isStale: true,
1254+
isFetching: false,
1255+
})
1256+
expect(states[1]).toMatchObject({
1257+
data: 'initial',
1258+
isStale: true,
1259+
isFetching: true,
1260+
})
1261+
expect(states[2]).toMatchObject({
1262+
data: 'data',
1263+
isStale: false,
1264+
isFetching: false,
1265+
})
1266+
expect(states[3]).toMatchObject({
1267+
data: 'data',
1268+
isStale: true,
1269+
isFetching: false,
1270+
})
1271+
})
1272+
12331273
it('should keep initial stale and initial data when the query key changes', async () => {
12341274
const key = queryKey()
12351275
const states: QueryResult<{ count: number }>[] = []
@@ -1251,15 +1291,13 @@ describe('useQuery', () => {
12511291

12521292
render(<Page />)
12531293

1254-
await waitFor(() => expect(states.length).toBe(5))
1294+
await waitForMs(100)
12551295

1256-
expect(states).toMatchObject([
1257-
{ data: { count: 0 } },
1258-
{ data: { count: 0 } },
1259-
{ data: { count: 1 } },
1260-
{ data: { count: 1 } },
1261-
{ data: { count: 10 } },
1262-
])
1296+
expect(states.length).toBe(4)
1297+
expect(states[0]).toMatchObject({ data: { count: 0 } })
1298+
expect(states[1]).toMatchObject({ data: { count: 0 } })
1299+
expect(states[2]).toMatchObject({ data: { count: 1 } })
1300+
expect(states[3]).toMatchObject({ data: { count: 1 } })
12631301
})
12641302

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

0 commit comments

Comments
 (0)