From c6c0de6ccdbb26dfdd4398008573e873c60bfef8 Mon Sep 17 00:00:00 2001 From: Niek Bosch Date: Thu, 3 Sep 2020 22:06:52 +0200 Subject: [PATCH] feat: add isPreviousData and isFetchedAfterMount flags (#961) --- docs/src/pages/docs/api.md | 10 +- src/core/config.ts | 12 +- src/core/query.ts | 21 ++-- src/core/queryObserver.ts | 14 ++- src/core/types.ts | 10 +- src/core/utils.ts | 2 +- src/react/tests/useInfiniteQuery.test.tsx | 12 +- src/react/tests/usePaginatedQuery.test.tsx | 7 +- src/react/tests/useQuery.test.tsx | 135 ++++++++++++++++++++- src/react/useBaseQuery.ts | 4 +- src/react/usePaginatedQuery.ts | 5 +- 11 files changed, 189 insertions(+), 43 deletions(-) diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md index 40e17818a7..5bad26f35c 100644 --- a/docs/src/pages/docs/api.md +++ b/docs/src/pages/docs/api.md @@ -12,9 +12,11 @@ const { error, failureCount, isError, + isFetchedAfterMount, isFetching, isIdle, isLoading, + isPreviousData, isStale, isSuccess, refetch, @@ -100,8 +102,7 @@ const queryInfo = useQuery({ - Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query. - `notifyOnStatusChange: Boolean` - Optional - - 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. + - Set this to `false` to only re-render when there are changes to `data` or `error`. - Defaults to `true`. - `onSuccess: Function(data) => data` - Optional @@ -170,6 +171,11 @@ const queryInfo = useQuery({ - The error object for the query, if an error was thrown. - `isStale: Boolean` - Will be `true` if the cache data is stale. +- `isPreviousData: Boolean` + - Will be `true` when `keepPreviousData` is set and data from the previous query is returned. +- `isFetchedAfterMount: Boolean` + - Will be `true` if the query has been fetched after the component mounted. + - This property can be used to not show any previously cached data. - `isFetching: Boolean` - Defaults to `true` so long as `manual` is set to `false` - Will be `true` if the query is currently fetching, including background fetching. diff --git a/src/core/config.ts b/src/core/config.ts index 801a2cb264..3fe3841730 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -51,16 +51,16 @@ export const DEFAULT_STALE_TIME = 0 export const DEFAULT_CACHE_TIME = 5 * 60 * 1000 export const DEFAULT_CONFIG: ReactQueryConfig = { queries: { - queryKeySerializerFn: defaultQueryKeySerializerFn, + cacheTime: DEFAULT_CACHE_TIME, enabled: true, + notifyOnStatusChange: true, + queryKeySerializerFn: defaultQueryKeySerializerFn, + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), staleTime: DEFAULT_STALE_TIME, - cacheTime: DEFAULT_CACHE_TIME, - refetchOnWindowFocus: true, - refetchOnReconnect: true, - refetchOnMount: true, - notifyOnStatusChange: true, structuralSharing: true, }, } diff --git a/src/core/query.ts b/src/core/query.ts index 49b1a9d140..00c0bc202e 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -9,6 +9,7 @@ import { isDocumentVisible, isOnline, isServer, + noop, replaceEqualDeep, sleep, } from './utils' @@ -38,6 +39,7 @@ export interface QueryState { data?: TResult error: TError | null failureCount: number + fetchedCount: number isError: boolean isFetched: boolean isFetching: boolean @@ -229,7 +231,7 @@ export class Query { ) } - async onWindowFocus(): Promise { + onWindowFocus(): void { if ( this.observers.some( observer => @@ -238,17 +240,13 @@ export class Query { observer.config.refetchOnWindowFocus ) ) { - try { - await this.fetch() - } catch { - // ignore - } + this.fetch().catch(noop) } this.continue() } - async onOnline(): Promise { + onOnline(): void { if ( this.observers.some( observer => @@ -257,11 +255,7 @@ export class Query { observer.config.refetchOnReconnect ) ) { - try { - await this.fetch() - } catch { - // ignore - } + this.fetch().catch(noop) } this.continue() @@ -612,6 +606,7 @@ function getDefaultState( isFetching: initialStatus === QueryStatus.Loading, isFetchingMore: false, failureCount: 0, + fetchedCount: 0, data: initialData, updatedAt: Date.now(), canFetchMore: hasMorePages(config, initialData), @@ -646,6 +641,7 @@ export function queryReducer( ...getStatusProps(QueryStatus.Success), data: action.data, error: null, + fetchedCount: state.fetchedCount + 1, isFetched: true, isFetching: false, isFetchingMore: false, @@ -658,6 +654,7 @@ export function queryReducer( ...state, ...getStatusProps(QueryStatus.Error), error: action.error, + fetchedCount: state.fetchedCount + 1, isFetched: true, isFetching: false, isFetchingMore: false, diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index ce9fe72c37..fc38b31601 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -15,6 +15,7 @@ export class QueryObserver { private currentResult!: QueryResult private previousQueryResult?: QueryResult private updateListener?: UpdateListener + private initialFetchedCount: number private staleTimeoutId?: number private refetchIntervalId?: number private started?: boolean @@ -22,6 +23,7 @@ export class QueryObserver { constructor(config: QueryObserverConfig) { this.config = config this.queryCache = config.queryCache! + this.initialFetchedCount = 0 // Bind exposed methods this.clear = this.clear.bind(this) @@ -100,6 +102,10 @@ export class QueryObserver { return this.currentResult.isStale } + getCurrentQuery(): Query { + return this.currentQuery + } + getCurrentResult(): QueryResult { return this.currentResult } @@ -224,16 +230,18 @@ export class QueryObserver { const { currentQuery, currentResult, previousQueryResult, config } = this const { state } = currentQuery let { data, status, updatedAt } = state + let isPreviousData = false // Keep previous data if needed if ( config.keepPreviousData && - state.isLoading && + (state.isIdle || state.isLoading) && previousQueryResult?.isSuccess ) { data = previousQueryResult.data updatedAt = previousQueryResult.updatedAt status = previousQueryResult.status + isPreviousData = true } let isStale = false @@ -261,10 +269,11 @@ export class QueryObserver { failureCount: state.failureCount, fetchMore: this.fetchMore, isFetched: state.isFetched, + isFetchedAfterMount: state.fetchedCount > this.initialFetchedCount, isFetching: state.isFetching, isFetchingMore: state.isFetchingMore, + isPreviousData, isStale, - query: currentQuery, refetch: this.refetch, updatedAt, } @@ -288,6 +297,7 @@ export class QueryObserver { this.previousQueryResult = this.currentResult this.currentQuery = newQuery + this.initialFetchedCount = newQuery.state.fetchedCount this.updateResult() if (this.started) { diff --git a/src/core/types.ts b/src/core/types.ts index 1a8f16a9be..9e363e8bb4 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,4 +1,4 @@ -import type { Query, FetchMoreOptions, RefetchOptions } from './query' +import type { FetchMoreOptions, RefetchOptions } from './query' import type { QueryCache } from './queryCache' export type QueryKey = @@ -34,11 +34,6 @@ export type QueryKeySerializerFunction = ( ) => [string, QueryKey[]] export interface BaseQueryConfig { - /** - * Set this to `false` to disable automatic refetching when the query mounts or changes query keys. - * To refetch the query, use the `refetch` method returned from the `useQuery` instance. - */ - enabled?: boolean | unknown /** * If `false`, failed queries will not retry by default. * If `true`, failed queries will retry infinitely., failureCount: num @@ -180,13 +175,14 @@ export interface QueryResultBase { ) => Promise isError: boolean isFetched: boolean + isFetchedAfterMount: boolean isFetching: boolean isFetchingMore?: IsFetchingMoreValue isIdle: boolean isLoading: boolean isStale: boolean isSuccess: boolean - query: Query + isPreviousData: boolean refetch: (options?: RefetchOptions) => Promise status: QueryStatus updatedAt: number diff --git a/src/core/utils.ts b/src/core/utils.ts index 2d8cb6e24a..25d0c9ab5d 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -29,7 +29,7 @@ export const uid = () => _uid++ export const isServer = typeof window === 'undefined' -function noop(): void { +export function noop(): void { return void 0 } diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx index 86e0d06777..f991efb535 100644 --- a/src/react/tests/useInfiniteQuery.test.tsx +++ b/src/react/tests/useInfiniteQuery.test.tsx @@ -68,13 +68,14 @@ describe('useInfiniteQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: false, + isFetchedAfterMount: false, isFetching: true, isFetchingMore: false, isIdle: false, isLoading: true, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), refetch: expect.any(Function), status: 'loading', updatedAt: expect.any(Number), @@ -96,12 +97,13 @@ describe('useInfiniteQuery', () => { isFetchingMore: false, isError: false, isFetched: true, + isFetchedAfterMount: true, isFetching: false, isIdle: false, isLoading: false, + isPreviousData: false, isStale: true, isSuccess: true, - query: expect.any(Object), refetch: expect.any(Function), status: 'success', updatedAt: expect.any(Number), @@ -152,36 +154,42 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingMore: false, isSuccess: false, + isPreviousData: false, }) expect(states[1]).toMatchObject({ data: ['0-desc'], isFetching: false, isFetchingMore: false, isSuccess: true, + isPreviousData: false, }) expect(states[2]).toMatchObject({ data: ['0-desc'], isFetching: true, isFetchingMore: 'next', isSuccess: true, + isPreviousData: false, }) expect(states[3]).toMatchObject({ data: ['0-desc', '1-desc'], isFetching: false, isFetchingMore: false, isSuccess: true, + isPreviousData: false, }) expect(states[4]).toMatchObject({ data: ['0-desc', '1-desc'], isFetching: true, isFetchingMore: false, isSuccess: true, + isPreviousData: true, }) expect(states[5]).toMatchObject({ data: ['0-asc'], isFetching: false, isFetchingMore: false, isSuccess: true, + isPreviousData: false, }) }) diff --git a/src/react/tests/usePaginatedQuery.test.tsx b/src/react/tests/usePaginatedQuery.test.tsx index 59f4119973..d54465216d 100644 --- a/src/react/tests/usePaginatedQuery.test.tsx +++ b/src/react/tests/usePaginatedQuery.test.tsx @@ -41,13 +41,14 @@ describe('usePaginatedQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: false, + isFetchedAfterMount: false, isFetching: true, isFetchingMore: false, isIdle: false, isLoading: true, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), latestData: undefined, resolvedData: undefined, refetch: expect.any(Function), @@ -64,13 +65,14 @@ describe('usePaginatedQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: true, + isFetchedAfterMount: true, isFetching: false, isFetchingMore: false, isIdle: false, isLoading: false, + isPreviousData: false, isStale: true, isSuccess: true, - query: expect.any(Object), latestData: 1, resolvedData: 1, refetch: expect.any(Function), @@ -208,6 +210,7 @@ describe('usePaginatedQuery', () => { }, { enabled: searchTerm, + keepPreviousData: page !== 1, } ) diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx index cc1882e7ef..8e1736faa8 100644 --- a/src/react/tests/useQuery.test.tsx +++ b/src/react/tests/useQuery.test.tsx @@ -130,13 +130,14 @@ describe('useQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: false, + isFetchedAfterMount: false, isFetching: true, isFetchingMore: false, isIdle: false, isLoading: true, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), refetch: expect.any(Function), status: 'loading', updatedAt: expect.any(Number), @@ -151,13 +152,14 @@ describe('useQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: true, + isFetchedAfterMount: true, isFetching: false, isFetchingMore: false, isIdle: false, isLoading: false, + isPreviousData: false, isStale: true, isSuccess: true, - query: expect.any(Object), refetch: expect.any(Function), status: 'success', updatedAt: expect.any(Number), @@ -202,13 +204,14 @@ describe('useQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: false, + isFetchedAfterMount: false, isFetching: true, isFetchingMore: false, isIdle: false, isLoading: true, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), refetch: expect.any(Function), status: 'loading', updatedAt: expect.any(Number), @@ -223,13 +226,14 @@ describe('useQuery', () => { fetchMore: expect.any(Function), isError: false, isFetched: false, + isFetchedAfterMount: false, isFetching: true, isFetchingMore: false, isIdle: false, isLoading: true, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), refetch: expect.any(Function), status: 'loading', updatedAt: expect.any(Number), @@ -244,13 +248,14 @@ describe('useQuery', () => { fetchMore: expect.any(Function), isError: true, isFetched: true, + isFetchedAfterMount: true, isFetching: false, isFetchingMore: false, isIdle: false, isLoading: false, + isPreviousData: false, isStale: true, isSuccess: false, - query: expect.any(Object), refetch: expect.any(Function), status: 'error', updatedAt: expect.any(Number), @@ -259,6 +264,39 @@ describe('useQuery', () => { consoleMock.mockRestore() }) + it('should set isFetchedAfterMount to true after a query has been fetched', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + await queryCache.prefetchQuery(key, () => 'prefetched') + + function Page() { + const state = useQuery(key, () => 'data') + states.push(state) + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(3)) + + expect(states[0]).toMatchObject({ + data: 'prefetched', + isFetched: true, + isFetchedAfterMount: false, + }) + expect(states[1]).toMatchObject({ + data: 'prefetched', + isFetched: true, + isFetchedAfterMount: false, + }) + expect(states[2]).toMatchObject({ + data: 'data', + isFetched: true, + isFetchedAfterMount: true, + }) + }) + // https://github.com/tannerlinsley/react-query/issues/896 it('should fetch data in Strict mode when refetchOnMount is false', async () => { const key = queryKey() @@ -442,21 +480,108 @@ describe('useQuery', () => { data: undefined, isFetching: true, isSuccess: false, + isPreviousData: false, }) expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, + isPreviousData: false, }) expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, + isPreviousData: true, }) expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, + isPreviousData: false, + }) + }) + + it('should keep the previous data on disabled query when keepPreviousData is set', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + function Page() { + const [count, setCount] = React.useState(0) + + const state = useQuery( + [key, count], + async () => { + await sleep(10) + return count + }, + { enabled: false, keepPreviousData: true } + ) + + states.push(state) + + const { refetch } = state + + React.useEffect(() => { + refetch() + + setTimeout(() => { + setCount(1) + }, 20) + + setTimeout(() => { + refetch() + }, 30) + }, [refetch]) + + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(6)) + + // Disabled query + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: false, + isSuccess: false, + isPreviousData: false, + }) + // Fetching query + expect(states[1]).toMatchObject({ + data: undefined, + isFetching: true, + isSuccess: false, + isPreviousData: false, + }) + // Fetched query + expect(states[2]).toMatchObject({ + data: 0, + isFetching: false, + isSuccess: true, + isPreviousData: false, + }) + // Switched query key + expect(states[3]).toMatchObject({ + data: 0, + isFetching: false, + isSuccess: true, + isPreviousData: true, + }) + // Fetching new query + expect(states[4]).toMatchObject({ + data: 0, + isFetching: true, + isSuccess: true, + isPreviousData: true, + }) + // Fetched new query + expect(states[5]).toMatchObject({ + data: 1, + isFetching: false, + isSuccess: true, + isPreviousData: false, }) }) diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts index f5c76f50d1..43023f2665 100644 --- a/src/react/useBaseQuery.ts +++ b/src/react/useBaseQuery.ts @@ -37,7 +37,9 @@ export function useBaseQuery( // Handle suspense if (config.suspense || config.useErrorBoundary) { - if (result.isError && result.query.state.throwInErrorBoundary) { + const query = observer.getCurrentQuery() + + if (result.isError && query.state.throwInErrorBoundary) { throw result.error } diff --git a/src/react/usePaginatedQuery.ts b/src/react/usePaginatedQuery.ts index a144bb2ab2..4eefce3e9b 100644 --- a/src/react/usePaginatedQuery.ts +++ b/src/react/usePaginatedQuery.ts @@ -58,13 +58,12 @@ export function usePaginatedQuery( ): PaginatedQueryResult { const config = getQueryArgs(args)[1] const result = useBaseQuery({ - ...config, keepPreviousData: true, + ...config, }) return { ...result, resolvedData: result.data, - latestData: - result.query.state.data === result.data ? result.data : undefined, + latestData: result.isPreviousData ? undefined : result.data, } }