Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface QueryState<TResult, TError> {
isFetching: boolean
isFetchingMore: IsFetchingMoreValue
isInitialData: boolean
isInvalidated: boolean
status: QueryStatus
throwInErrorBoundary?: boolean
updateCount: number
Expand All @@ -58,6 +59,7 @@ const enum ActionType {
Fetch,
Success,
Error,
Invalidate,
}

interface SetDataOptions {
Expand Down Expand Up @@ -85,10 +87,15 @@ interface ErrorAction<TError> {
error: TError
}

interface InvalidateAction {
type: ActionType.Invalidate
}

export type Action<TResult, TError> =
| ErrorAction<TError>
| FailedAction
| FetchAction
| InvalidateAction
| SuccessAction<TResult>

// CLASS
Expand Down Expand Up @@ -226,16 +233,21 @@ export class Query<TResult, TError> {
this.cancel()
}

isEnabled(): boolean {
isActive(): boolean {
return this.observers.some(observer => observer.config.enabled)
}

isStale(): boolean {
return this.observers.some(observer => observer.getCurrentResult().isStale)
return (
this.state.isInvalidated ||
this.state.status !== QueryStatus.Success ||
this.observers.some(observer => observer.getCurrentResult().isStale)
)
}

isStaleByTime(staleTime = 0): boolean {
return (
this.state.isInvalidated ||
this.state.status !== QueryStatus.Success ||
this.state.updatedAt + staleTime <= Date.now()
)
Expand Down Expand Up @@ -295,6 +307,12 @@ export class Query<TResult, TError> {
}
}

invalidate(): void {
if (!this.state.isInvalidated) {
this.dispatch({ type: ActionType.Invalidate })
}
}

async refetch(
options?: RefetchOptions,
config?: ResolvedQueryConfig<TResult, TError>
Expand Down Expand Up @@ -610,6 +628,7 @@ function getDefaultState<TResult, TError>(
isFetching: status === QueryStatus.Loading,
isFetchingMore: false,
isInitialData: true,
isInvalidated: false,
status,
updateCount: 0,
updatedAt: Date.now(),
Expand Down Expand Up @@ -647,6 +666,7 @@ export function queryReducer<TResult, TError>(
isFetching: false,
isFetchingMore: false,
isInitialData: false,
isInvalidated: false,
status: QueryStatus.Success,
updateCount: state.updateCount + 1,
updatedAt: action.updatedAt ?? Date.now(),
Expand All @@ -662,6 +682,11 @@ export function queryReducer<TResult, TError>(
throwInErrorBoundary: true,
updateCount: state.updateCount + 1,
}
case ActionType.Invalidate:
return {
...state,
isInvalidated: true,
}
default:
return state
}
Expand Down
102 changes: 74 additions & 28 deletions src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
deepIncludes,
getQueryArgs,
isDocumentVisible,
isPlainObject,
isOnline,
isPlainObject,
isServer,
} from './utils'
import { getResolvedQueryConfig } from './config'
Expand Down Expand Up @@ -35,14 +35,19 @@ interface PrefetchQueryOptions {
throwOnError?: boolean
}

interface InvalidateQueriesOptions extends QueryPredicateOptions {
interface RefetchQueriesOptions extends QueryPredicateOptions {
throwOnError?: boolean
}

interface InvalidateQueriesOptions extends RefetchQueriesOptions {
refetchActive?: boolean
refetchInactive?: boolean
throwOnError?: boolean
}

interface QueryPredicateOptions {
active?: boolean
exact?: boolean
stale?: boolean
}

type QueryPredicate = QueryKey | QueryPredicateFn | true
Expand Down Expand Up @@ -123,7 +128,7 @@ export class QueryCache {
predicate?: QueryPredicate,
options?: QueryPredicateOptions
): Query<TResult, TError>[] {
if (predicate === true || typeof predicate === 'undefined') {
if (!options && (predicate === true || typeof predicate === 'undefined')) {
return this.queriesArray
}

Expand All @@ -134,10 +139,19 @@ export class QueryCache {
} else {
const resolvedConfig = this.getResolvedQueryConfig(predicate)

predicateFn = d =>
options?.exact
? d.queryHash === resolvedConfig.queryHash
: deepIncludes(d.queryKey, resolvedConfig.queryKey)
predicateFn = query => {
if (
options &&
((options.exact && query.queryHash !== resolvedConfig.queryHash) ||
(typeof options.active === 'boolean' &&
query.isActive() !== options.active) ||
(typeof options.stale === 'boolean' &&
query.isStale() !== options.stale))
) {
return false
}
return deepIncludes(query.queryKey, resolvedConfig.queryKey)
}
}

return this.queriesArray.filter(predicateFn)
Expand Down Expand Up @@ -186,30 +200,62 @@ export class QueryCache {
})
}

async invalidateQueries(
/**
* @return Promise resolving to an array with the invalidated queries.
*/
invalidateQueries(
predicate?: QueryPredicate,
options?: InvalidateQueriesOptions
): Promise<void> {
const { refetchActive = true, refetchInactive = false, throwOnError } =
options || {}
): Promise<Query<unknown, unknown>[]> {
const queries = this.getQueries(predicate, options)

try {
await Promise.all(
this.getQueries(predicate, options).map(query => {
const enabled = query.isEnabled()

if ((enabled && refetchActive) || (!enabled && refetchInactive)) {
return query.fetch()
}

return undefined
})
)
} catch (err) {
if (throwOnError) {
throw err
}
queries.forEach(query => {
query.invalidate()
})

const { refetchActive = true, refetchInactive = false } = options || {}

if (!refetchInactive && !refetchActive) {
return Promise.resolve(queries)
}

const refetchOptions: RefetchQueriesOptions = { ...options }

if (refetchActive && !refetchInactive) {
refetchOptions.active = true
} else if (refetchInactive && !refetchActive) {
refetchOptions.active = false
}

let promise = this.refetchQueries(predicate, refetchOptions)

if (!options?.throwOnError) {
promise = promise.catch(() => queries)
}

return promise.then(() => queries)
}

/**
* @return Promise resolving to an array with the refetched queries.
*/
refetchQueries(
predicate?: QueryPredicate,
options?: RefetchQueriesOptions
): Promise<Query<unknown, unknown>[]> {
const promises: Promise<Query<unknown, unknown>>[] = []

this.getQueries(predicate, options).forEach(query => {
let promise = query.fetch().then(() => query)

if (!options?.throwOnError) {
promise = promise.catch(() => query)
}

promises.push(promise)
})

return Promise.all(promises)
}

resetErrorBoundaries(): void {
Expand Down
18 changes: 13 additions & 5 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,8 @@ export class QueryObserver<TResult, TError> {
const { config } = this
const { type } = action

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

Expand All @@ -316,15 +316,23 @@ export class QueryObserver<TResult, TError> {
this.updateResult()
const currentResult = this.currentResult

// Trigger callbacks and timers on success or error
// Update timers on success, error or invalidation
if (type === 2 || type === 3 || type === 4) {
this.updateTimers()
}

// Trigger callbacks on success or error
if (type === 2) {
config.onSuccess?.(currentResult.data!)
config.onSettled?.(currentResult.data!, null)
this.updateTimers()
} else if (type === 3) {
config.onError?.(currentResult.error!)
config.onSettled?.(undefined, currentResult.error!)
this.updateTimers()
}

// Do not notify if the query was invalidated but the stale state did not changed
if (type === 4 && currentResult.isStale === prevResult.isStale) {
return
}

if (
Expand Down
31 changes: 31 additions & 0 deletions src/core/tests/queryCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,37 @@ describe('queryCache', () => {
expect(data).toEqual(['data1', 'data2'])
})

test('getQueries should filter correctly', async () => {
const key1 = queryKey()
const key2 = queryKey()
const cache = defaultQueryCache

await cache.prefetchQuery(key1, () => 'data1')
await cache.prefetchQuery(key2, () => 'data2')
await cache.invalidateQueries(key2)
const query1 = cache.getQuery(key1)!
const query2 = cache.getQuery(key2)!

expect(cache.getQueries(key1)).toEqual([query1])
expect(cache.getQueries(key1, {})).toEqual([query1])
expect(cache.getQueries(key1, { active: false })).toEqual([query1])
expect(cache.getQueries(key1, { active: true })).toEqual([])
expect(cache.getQueries(key1, { stale: true })).toEqual([])
expect(cache.getQueries(key1, { stale: false })).toEqual([query1])
expect(cache.getQueries(key1, { stale: false, active: true })).toEqual([])
expect(cache.getQueries(key1, { stale: false, active: false })).toEqual([
query1,
])
expect(
cache.getQueries(key1, { stale: false, active: false, exact: true })
).toEqual([query1])

expect(cache.getQueries(key2)).toEqual([query2])
expect(cache.getQueries(key2, { stale: undefined })).toEqual([query2])
expect(cache.getQueries(key2, { stale: true })).toEqual([query2])
expect(cache.getQueries(key2, { stale: false })).toEqual([])
})

test('query interval is cleared when unsubscribed to a refetchInterval query', async () => {
const key = queryKey()

Expand Down
Loading