From 4cffe2e70426f0fff3e68f6cbfa31747aced8f62 Mon Sep 17 00:00:00 2001 From: Niek Date: Wed, 2 Sep 2020 15:57:21 +0200 Subject: [PATCH] feat: add forceFetchOnMount flag --- docs/src/pages/docs/api.md | 5 ++ src/core/query.ts | 2 +- src/core/queryObserver.ts | 84 +++++++++++++++---------------- src/core/types.ts | 5 ++ src/react/tests/useQuery.test.tsx | 74 ++++++++++++++++++++++++++- 5 files changed, 125 insertions(+), 45 deletions(-) diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md index ed71f8c25b..40e17818a7 100644 --- a/docs/src/pages/docs/api.md +++ b/docs/src/pages/docs/api.md @@ -22,6 +22,7 @@ const { } = useQuery(queryKey, queryFn?, { cacheTime, enabled, + forceFetchOnMount, initialData, initialStale, isDataEqual, @@ -128,6 +129,10 @@ const queryInfo = useQuery({ - Optional - Defaults to `false` - If set, any previous `data` will be kept when fetching new data because the query key changed. +- `forceFetchOnMount: Boolean` + - Optional + - Defaults to `false` + - Set this to `true` to always fetch when the component mounts (regardless of staleness). - `refetchOnMount: Boolean` - Optional - Defaults to `true` diff --git a/src/core/query.ts b/src/core/query.ts index e731472aa0..d902a21468 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -135,7 +135,7 @@ export class Query { this.state = queryReducer(this.state, action) this.observers.forEach(observer => { - observer.onQueryUpdate(this.state, action) + observer.onQueryUpdate(action) }) this.notifyGlobalListeners(this) diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index e8cb6c5f45..bad9dbd3e2 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -1,12 +1,6 @@ import { getStatusProps, isServer, isDocumentVisible } from './utils' import type { QueryResult, QueryObserverConfig } from './types' -import type { - Query, - QueryState, - Action, - FetchMoreOptions, - RefetchOptions, -} from './query' +import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query' import type { QueryCache } from './queryCache' export type UpdateListener = ( @@ -19,7 +13,7 @@ export class QueryObserver { private queryCache: QueryCache private currentQuery!: Query private currentResult!: QueryResult - private previousResult?: QueryResult + private previousQueryResult?: QueryResult private updateListener?: UpdateListener private staleTimeoutId?: number private refetchIntervalId?: number @@ -42,7 +36,13 @@ export class QueryObserver { this.started = true this.updateListener = listener this.currentQuery.subscribeObserver(this) - this.optionalFetch() + + if (this.config.enabled && this.config.forceFetchOnMount) { + this.fetch() + } else { + this.optionalFetch() + } + this.updateTimers() return this.unsubscribe.bind(this) } @@ -141,11 +141,15 @@ export class QueryObserver { private updateIsStale(): void { const isStale = this.currentQuery.isStaleByTime(this.config.staleTime) if (isStale !== this.currentResult.isStale) { - this.currentResult = this.createResult() - this.updateListener?.(this.currentResult) + this.updateResult() + this.notify() } } + private notify(): void { + this.updateListener?.(this.currentResult) + } + private updateStaleTimeout(): void { if (isServer) { return @@ -216,8 +220,8 @@ export class QueryObserver { } } - private createResult(): QueryResult { - const { currentQuery, currentResult, previousResult, config } = this + private updateResult(): void { + const { currentQuery, currentResult, previousQueryResult, config } = this const { state } = currentQuery let { data, status, updatedAt } = state @@ -225,30 +229,30 @@ export class QueryObserver { if ( config.keepPreviousData && state.isLoading && - previousResult?.isSuccess + previousQueryResult?.isSuccess ) { - data = previousResult.data - updatedAt = previousResult.updatedAt - status = previousResult.status + data = previousQueryResult.data + updatedAt = previousQueryResult.updatedAt + status = previousQueryResult.status } let isStale = false // 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 && !currentQuery.state.isFetched) { + if (!currentResult && !state.isFetched) { if (typeof config.initialStale === 'function') { isStale = config.initialStale() } else if (typeof config.initialStale === 'boolean') { isStale = config.initialStale } else { - isStale = typeof currentQuery.state.data === 'undefined' + isStale = typeof state.data === 'undefined' } } else { isStale = currentQuery.isStaleByTime(config.staleTime) } - return { + this.currentResult = { ...getStatusProps(status), canFetchMore: state.canFetchMore, clear: this.clear, @@ -282,9 +286,9 @@ export class QueryObserver { return false } - this.previousResult = this.currentResult + this.previousQueryResult = this.currentResult this.currentQuery = newQuery - this.currentResult = this.createResult() + this.updateResult() if (this.started) { prevQuery?.unsubscribeObserver(this) @@ -294,39 +298,33 @@ export class QueryObserver { return true } - onQueryUpdate( - _state: QueryState, - action: Action - ): void { - const { config } = this - + onQueryUpdate(action: Action): void { // Store current result and get new result const prevResult = this.currentResult - this.currentResult = this.createResult() - const result = this.currentResult + this.updateResult() + + const { currentResult, config } = this // We need to check the action because the state could have // transitioned from success to success in case of `setQueryData`. - if (action.type === 'Success' && result.isSuccess) { - config.onSuccess?.(result.data!) - config.onSettled?.(result.data!, null) + if (action.type === 'Success' && currentResult.isSuccess) { + config.onSuccess?.(currentResult.data!) + config.onSettled?.(currentResult.data!, null) this.updateTimers() - } else if (action.type === 'Error' && result.isError) { - config.onError?.(result.error!) - config.onSettled?.(undefined, result.error!) + } else if (action.type === 'Error' && currentResult.isError) { + config.onError?.(currentResult.error!) + config.onSettled?.(undefined, currentResult.error!) this.updateTimers() } - // Decide if we need to notify the listener - const notify = + if ( // Always notify on data or error change - result.data !== prevResult.data || - result.error !== prevResult.error || + currentResult.data !== prevResult.data || + currentResult.error !== prevResult.error || // Maybe notify on other changes config.notifyOnStatusChange - - if (notify) { - this.updateListener?.(result) + ) { + this.notify() } } } diff --git a/src/core/types.ts b/src/core/types.ts index fccead863f..1a8f16a9be 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -105,6 +105,11 @@ export interface QueryObserverConfig< * Defaults to `true`. */ refetchOnMount?: boolean + /** + * Set this to `true` to always fetch when the component mounts (regardless of staleness). + * Defaults to `false`. + */ + forceFetchOnMount?: boolean /** * Whether a change to the query status should re-render a component. * If set to `false`, the component will only re-render when the actual `data` or `error` changes. diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx index abc2c9c411..cc1882e7ef 100644 --- a/src/react/tests/useQuery.test.tsx +++ b/src/react/tests/useQuery.test.tsx @@ -357,7 +357,7 @@ describe('useQuery', () => { return null }) - it.only('should update disabled query when updated with invalidateQueries', async () => { + it('should update disabled query when updated with invalidateQueries', async () => { const key = queryKey() const states: QueryResult[] = [] let count = 0 @@ -843,6 +843,78 @@ describe('useQuery', () => { consoleMock.mockRestore() }) + it('should always fetch if forceFetchOnMount is set', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + await queryCache.prefetchQuery(key, () => 'prefetched') + + function Page() { + const state = useQuery(key, () => 'data', { + forceFetchOnMount: true, + staleTime: 100, + }) + states.push(state) + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(3)) + + expect(states).toMatchObject([ + { data: 'prefetched', isStale: false, isFetching: false }, + { data: 'prefetched', isStale: false, isFetching: true }, + { data: 'data', isStale: false, isFetching: false }, + ]) + }) + + it('should not fetch if initial data is set', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + function Page() { + const state = useQuery(key, () => 'data', { + initialData: 'initial', + }) + states.push(state) + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(2)) + + expect(states).toMatchObject([ + { data: 'initial', isStale: false }, + { data: 'initial', isStale: true }, + ]) + }) + + it('should fetch if initial data is set and initial stale is set to true', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + function Page() { + const state = useQuery(key, () => 'data', { + initialData: 'initial', + initialStale: true, + }) + states.push(state) + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(3)) + + expect(states).toMatchObject([ + { data: 'initial', isStale: true, isFetching: false }, + { data: 'initial', isStale: true, isFetching: true }, + { 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 }>[] = []