Skip to content

Commit

Permalink
fix: make sure initial data always uses initial stale (#1010)
Browse files Browse the repository at this point in the history
  • Loading branch information
boschni committed Sep 13, 2020
1 parent 380a049 commit 460ab33
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 51 deletions.
76 changes: 38 additions & 38 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
isValidTimeout,
noop,
} from './utils'
import { QueryResult, ResolvedQueryConfig, QueryStatus } from './types'
import type { QueryResult, ResolvedQueryConfig } from './types'
import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query'

export type UpdateListener<TResult, TError> = (
Expand All @@ -19,12 +19,14 @@ export class QueryObserver<TResult, TError> {
private currentResult!: QueryResult<TResult, TError>
private previousQueryResult?: QueryResult<TResult, TError>
private listener?: UpdateListener<TResult, TError>
private isStale: boolean
private initialUpdateCount: number
private staleTimeoutId?: number
private refetchIntervalId?: number

constructor(config: ResolvedQueryConfig<TResult, TError>) {
this.config = config
this.isStale = true
this.initialUpdateCount = 0

// Bind exposed methods
Expand Down Expand Up @@ -128,9 +130,9 @@ export class QueryObserver<TResult, TError> {

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

this.clearStaleTimeout()

if (this.currentResult.isStale || !isValidTimeout(this.config.staleTime)) {
if (this.isStale || !isValidTimeout(this.config.staleTime)) {
return
}

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

this.staleTimeoutId = setTimeout(() => {
if (!this.currentResult.isStale) {
this.currentResult = { ...this.currentResult, isStale: true }
if (!this.isStale) {
this.isStale = true
this.updateResult()
this.notify()
this.config.queryCache.notifyGlobalListeners(this.currentQuery)
}
Expand Down Expand Up @@ -208,40 +211,23 @@ export class QueryObserver<TResult, TError> {
}

private updateResult(): void {
const { currentQuery, currentResult, previousQueryResult, config } = this
const { currentQuery, previousQueryResult, config } = this
const { state } = currentQuery
let { data, status, updatedAt } = state
let isPreviousData = false

// Keep previous data if needed
if (
config.keepPreviousData &&
(state.status === QueryStatus.Idle ||
state.status === QueryStatus.Loading) &&
previousQueryResult?.status === QueryStatus.Success
state.isInitialData &&
previousQueryResult?.isSuccess
) {
data = previousQueryResult.data
updatedAt = previousQueryResult.updatedAt
status = previousQueryResult.status
isPreviousData = true
}

let isStale

// When the query has not been fetched yet and this is the initial render,
// determine the staleness based on the initialStale or existence of initial data.
if (!currentResult && state.isInitialData) {
if (typeof config.initialStale === 'function') {
isStale = config.initialStale()
} else if (typeof config.initialStale === 'boolean') {
isStale = config.initialStale
} else {
isStale = typeof state.data === 'undefined'
}
} else {
isStale = currentQuery.isStaleByTime(config.staleTime)
}

this.currentResult = {
...getStatusProps(status),
canFetchMore: state.canFetchMore,
Expand All @@ -256,22 +242,16 @@ export class QueryObserver<TResult, TError> {
isFetchingMore: state.isFetchingMore,
isInitialData: state.isInitialData,
isPreviousData,
isStale,
isStale: this.isStale,
refetch: this.refetch,
updatedAt,
}
}

private updateQuery(): void {
const config = this.config
const prevQuery = this.currentQuery

// Remove the initial data when there is an existing query
// because this data should not be used for a new query
const config =
this.config.keepPreviousData && prevQuery
? { ...this.config, initialData: undefined }
: this.config

let query = config.queryCache.getQueryByHash<TResult, TError>(
config.queryHash
)
Expand All @@ -287,6 +267,22 @@ export class QueryObserver<TResult, TError> {
this.previousQueryResult = this.currentResult
this.currentQuery = query
this.initialUpdateCount = query.state.updateCount

// Update stale state on query switch
if (query.state.isInitialData) {
if (config.keepPreviousData && prevQuery) {
this.isStale = true
} else if (typeof config.initialStale === 'function') {
this.isStale = config.initialStale()
} else if (typeof config.initialStale === 'boolean') {
this.isStale = config.initialStale
} else {
this.isStale = typeof query.state.data === 'undefined'
}
} else {
this.isStale = query.isStaleByTime(config.staleTime)
}

this.updateResult()

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

onQueryUpdate(action: Action<TResult, TError>): void {
const { config } = this
const { type } = action

// Update stale state on success or error
if (type === 2 || type === 3) {
this.isStale = this.currentQuery.isStaleByTime(config.staleTime)
}

// Store current result and get new result
const prevResult = this.currentResult
this.updateResult()
const currentResult = this.currentResult

const { currentResult, config } = this

// We need to check the action because the state could have
// transitioned from success to success in case of `setQueryData`.
// Trigger callbacks and timers on success or error
if (type === 2) {
config.onSuccess?.(currentResult.data!)
config.onSettled?.(currentResult.data!, null)
Expand Down
64 changes: 51 additions & 13 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1198,12 +1198,11 @@ describe('useQuery', () => {

render(<Page />)

await waitFor(() => expect(states.length).toBe(2))
await waitForMs(10)

expect(states).toMatchObject([
{ data: 'initial', isStale: false },
{ data: 'initial', isStale: true },
])
expect(states.length).toBe(2)
expect(states[0]).toMatchObject({ data: 'initial', isStale: false })
expect(states[1]).toMatchObject({ data: 'initial', isStale: true })
})

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

it('should fetch if initial data is set and initial stale is set to true with stale time', async () => {
const key = queryKey()
const states: QueryResult<string>[] = []

function Page() {
const state = useQuery(key, () => 'data', {
staleTime: 50,
initialData: 'initial',
initialStale: true,
})
states.push(state)
return null
}

render(<Page />)

await waitForMs(100)

expect(states.length).toBe(4)
expect(states[0]).toMatchObject({
data: 'initial',
isStale: true,
isFetching: false,
})
expect(states[1]).toMatchObject({
data: 'initial',
isStale: true,
isFetching: true,
})
expect(states[2]).toMatchObject({
data: 'data',
isStale: false,
isFetching: false,
})
expect(states[3]).toMatchObject({
data: 'data',
isStale: true,
isFetching: false,
})
})

it('should keep initial stale and initial data when the query key changes', async () => {
const key = queryKey()
const states: QueryResult<{ count: number }>[] = []
Expand All @@ -1251,15 +1291,13 @@ describe('useQuery', () => {

render(<Page />)

await waitFor(() => expect(states.length).toBe(5))
await waitForMs(100)

expect(states).toMatchObject([
{ data: { count: 0 } },
{ data: { count: 0 } },
{ data: { count: 1 } },
{ data: { count: 1 } },
{ data: { count: 10 } },
])
expect(states.length).toBe(4)
expect(states[0]).toMatchObject({ data: { count: 0 } })
expect(states[1]).toMatchObject({ data: { count: 0 } })
expect(states[2]).toMatchObject({ data: { count: 1 } })
expect(states[3]).toMatchObject({ data: { count: 1 } })
})

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

1 comment on commit 460ab33

@vercel
Copy link

@vercel vercel bot commented on 460ab33 Sep 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.