From 304d0dd3c39d9b1273c3c0efe5cdda819ac587a4 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Tue, 27 Dec 2022 23:43:56 +0100 Subject: [PATCH 01/13] feat: remove `keepPreviousData` in favor of `placeholderData` BREAKING CHANGE: removed `keepPreviousData` in favor of `placeholderData` identity function --- .../guides/migrating-to-react-query-5.md | 46 ++++++- docs/react/guides/paginated-queries.md | 20 +-- docs/react/reference/QueryClient.md | 2 +- docs/react/reference/useQuery.md | 13 +- examples/react/pagination/pages/index.js | 10 +- packages/query-core/src/queriesObserver.ts | 17 +-- packages/query-core/src/queryObserver.ts | 26 +--- packages/query-core/src/types.ts | 10 +- .../src/__tests__/useInfiniteQuery.test.tsx | 21 ++- .../src/__tests__/useQueries.test.tsx | 126 ++++++++++++++---- .../src/__tests__/useQuery.test.tsx | 88 ++++++------ .../__tests__/createInfiniteQuery.test.tsx | 18 ++- .../src/__tests__/createQueries.test.tsx | 81 +++++++---- .../src/__tests__/createQuery.test.tsx | 57 ++++---- 14 files changed, 316 insertions(+), 219 deletions(-) diff --git a/docs/react/guides/migrating-to-react-query-5.md b/docs/react/guides/migrating-to-react-query-5.md index a57341a479..c260a7c838 100644 --- a/docs/react/guides/migrating-to-react-query-5.md +++ b/docs/react/guides/migrating-to-react-query-5.md @@ -150,14 +150,50 @@ If you want to throw something that isn't an Error, you'll now have to set the g useQuery({ queryKey: ['some-query'], queryFn: async () => { - if (Math.random() > 0.5) { - throw 'some error' - } - return 42 - } + if (Math.random() > 0.5) { + throw 'some error' + } + return 42 + }, }) ``` ### eslint `prefer-query-object-syntax` rule is removed Since the only supported syntax now is the object syntax, this rule is no longer needed + +### Removed `keepPreviousData` in favor of `placeholderData` identity function + +We have removed the `keepPreviousData` option and `isPreviousData` flag as they were doing mostly the same thing as `placeholderData` and `isPlaceholderData` flag. + +To achieve the same functionality as `keepPreviousData`, we have added previous query `data` as an argument to `placeholderData` function. +Therefore you just need to provide an identity function to `placeholderData` + +```diff +useQuery({ + queryKey, + queryFn, +- keepPreviousData: true, ++ placeholderData: (previousData) => previousData +}); +``` + +There are some caveats to this change however, which you must be aware of: + +- `placeholderData` will always put you into `success` state, while `keepPreviousData` will give you the status of the previous query. That status could be `error` if we have data fetched successfully and then got a background refetch error. However, the error itself is not shared. This doesn't seem quite right in any case. Therefore we decided to stick with behavior of `placeholderData`. +- `keepPreviousData` will give you the `dataUpdatedAt` timestamp of the previous data, while with `placeholderData`, `dataUpdatedAt` will stay at `0`. This might be annoying if you want to show that timestamp continuously on screen. However you might get around it with `useEffect`. + + ```ts + const [updatedAt, setUpdatedAt] = useState(0) + + const { data, dataUpdatedAt } = useQuery({ + queryKey: ['projects', page], + queryFn: () => fetchProjects(page), + }) + + useEffect(() => { + if (dataUpdatedAt > updatedAt) { + setUpdatedAt(dataUpdatedAt) + } + }, [dataUpdatedAt]) + ``` diff --git a/docs/react/guides/paginated-queries.md b/docs/react/guides/paginated-queries.md index b1bbe85f4d..965361b613 100644 --- a/docs/react/guides/paginated-queries.md +++ b/docs/react/guides/paginated-queries.md @@ -18,15 +18,15 @@ However, if you run this simple example, you might notice something strange: **The UI jumps in and out of the `success` and `loading` states because each new page is treated like a brand new query.** -This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called `keepPreviousData` that allows us to get around this. +This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called `placeholderData` that allows us to get around this. -## Better Paginated Queries with `keepPreviousData` +## Better Paginated Queries with `placeholderData` -Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `keepPreviousData` to `true` we get a few new things: +Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `placeholderData` to `(previousData) => previousData` we get a few new things: - **The data from the last successful fetch available while new data is being requested, even though the query key has changed**. - When the new data arrives, the previous `data` is seamlessly swapped to show the new data. -- `isPreviousData` is made available to know what data the query is currently providing you +- `isPlaceholderData` is made available to know what data the query is currently providing you [//]: # 'Example2' ```tsx @@ -41,11 +41,11 @@ function Todos() { error, data, isFetching, - isPreviousData, + isPlaceholderData, } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), - keepPreviousData : true + placeholderData: (previousData) => previousData, }) return ( @@ -70,12 +70,12 @@ function Todos() { {' '} @@ -86,6 +86,6 @@ function Todos() { ``` [//]: # 'Example2' -## Lagging Infinite Query results with `keepPreviousData` +## Lagging Infinite Query results with `placeholderData` -While not as common, the `keepPreviousData` option also works flawlessly with the `useInfiniteQuery` hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. +While not as common, the `placeholderData` option also works flawlessly with the `useInfiniteQuery` hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. diff --git a/docs/react/reference/QueryClient.md b/docs/react/reference/QueryClient.md index de01824703..0453e71056 100644 --- a/docs/react/reference/QueryClient.md +++ b/docs/react/reference/QueryClient.md @@ -95,7 +95,7 @@ try { **Options** -The options for `fetchQuery` are exactly the same as those of [`useQuery`](../reference/useQuery), except the following: `enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, onSuccess, onError, onSettled, throwErrors, select, suspense, keepPreviousData, placeholderData`; which are strictly for useQuery and useInfiniteQuery. You can check the [source code](https://github.com/tannerlinsley/react-query/blob/361935a12cec6f36d0bd6ba12e84136c405047c5/src/core/types.ts#L83) for more clarity. +The options for `fetchQuery` are exactly the same as those of [`useQuery`](../reference/useQuery), except the following: `enabled, refetchInterval, refetchIntervalInBackground, refetchOnWindowFocus, refetchOnReconnect, notifyOnChangeProps, onSuccess, onError, onSettled, throwErrors, select, suspense, placeholderData`; which are strictly for useQuery and useInfiniteQuery. You can check the [source code](https://github.com/tannerlinsley/react-query/blob/361935a12cec6f36d0bd6ba12e84136c405047c5/src/core/types.ts#L83) for more clarity. **Returns** diff --git a/docs/react/reference/useQuery.md b/docs/react/reference/useQuery.md index 9358686904..175d102705 100644 --- a/docs/react/reference/useQuery.md +++ b/docs/react/reference/useQuery.md @@ -19,7 +19,6 @@ const { isLoading, isLoadingError, isPlaceholderData, - isPreviousData, isRefetchError, isRefetching, isStale, @@ -35,7 +34,6 @@ const { networkMode, initialData, initialDataUpdatedAt, - keepPreviousData, meta, notifyOnChangeProps, onError, @@ -160,15 +158,12 @@ const { - `initialDataUpdatedAt: number | (() => number | undefined)` - Optional - If set, this value will be used as the time (in milliseconds) of when the `initialData` itself was last updated. -- `placeholderData: TData | () => TData` +- `placeholderData: TData | (previousValue: TData) => TData` - Optional - If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. - `placeholderData` is **not persisted** to the cache -- `keepPreviousData: boolean` - - Optional - - Defaults to `false` - - If set, any previous `data` will be kept when fetching new data because the query key changed. - `structuralSharing: boolean | ((oldData: TData | undefined, newData: TData) => TData)` + - If you provide a function for `placeholderData`, as a first argument you will receive previously watched query data if available +- `structuralSharing: boolean | ((oldData: TData | undefined, newData: TData) => TData)` - Optional - Defaults to `true` - If set to `false`, structural sharing between query results will be disabled. @@ -215,8 +210,6 @@ const { - Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`. - `isPlaceholderData: boolean` - Will be `true` if the data shown is the placeholder data. -- `isPreviousData: boolean` - - Will be `true` when `keepPreviousData` is set and data from the previous query is returned. - `isFetched: boolean` - Will be `true` if the query has been fetched. - `isFetchedAfterMount: boolean` diff --git a/examples/react/pagination/pages/index.js b/examples/react/pagination/pages/index.js index 8e63d685bb..1ade0de83a 100644 --- a/examples/react/pagination/pages/index.js +++ b/examples/react/pagination/pages/index.js @@ -27,22 +27,22 @@ function Example() { const queryClient = useQueryClient() const [page, setPage] = React.useState(0) - const { status, data, error, isFetching, isPreviousData } = useQuery({ + const { status, data, error, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), - keepPreviousData: true, + placeholderData: (previousData) => previousData, staleTime: 5000, }) // Prefetch the next page! React.useEffect(() => { - if (!isPreviousData && data?.hasMore) { + if (!isPlaceholderData && data?.hasMore) { queryClient.prefetchQuery({ queryKey: ['projects', page + 1], queryFn: () => fetchProjects(page + 1), }) } - }, [data, isPreviousData, page, queryClient]) + }, [data, isPlaceholderData, page, queryClient]) return (
@@ -78,7 +78,7 @@ function Example() { onClick={() => { setPage((old) => (data?.hasMore ? old + 1 : old)) }} - disabled={isPreviousData || !data?.hasMore} + disabled={isPlaceholderData || !data?.hasMore} > Next Page diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index c9fbc5e164..b3a7dbddae 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -155,11 +155,6 @@ export class QueriesObserver extends Subscribable { !matchedQueryHashes.includes(defaultedOptions.queryHash), ) - const unmatchedObservers = prevObservers.filter( - (prevObserver) => - !matchingObservers.some((match) => match.observer === prevObserver), - ) - const getObserver = (options: QueryObserverOptions): QueryObserver => { const defaultedOptions = this.client.defaultQueryOptions(options) const currentObserver = this.observersMap[defaultedOptions.queryHash!] @@ -167,17 +162,7 @@ export class QueriesObserver extends Subscribable { } const newOrReusedObservers: QueryObserverMatch[] = unmatchedQueries.map( - (options, index) => { - if (options.keepPreviousData) { - // return previous data from one of the observers that no longer match - const previouslyUsedObserver = unmatchedObservers[index] - if (previouslyUsedObserver !== undefined) { - return { - defaultedQueryOptions: options, - observer: previouslyUsedObserver, - } - } - } + (options) => { return { defaultedQueryOptions: options, observer: getObserver(options), diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 0dcb1bbb2c..31dc3acc32 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -422,8 +422,7 @@ export class QueryObserver< : this.previousQueryResult const { state } = query - let { dataUpdatedAt, error, errorUpdatedAt, fetchStatus, status } = state - let isPreviousData = false + let { error, errorUpdatedAt, fetchStatus, status } = state let isPlaceholderData = false let data: TData | undefined @@ -440,7 +439,7 @@ export class QueryObserver< fetchStatus = canFetch(query.options.networkMode) ? 'fetching' : 'paused' - if (!dataUpdatedAt) { + if (!state.dataUpdatedAt) { status = 'loading' } } @@ -449,20 +448,8 @@ export class QueryObserver< } } - // Keep previous data if needed - if ( - options.keepPreviousData && - !state.dataUpdatedAt && - prevQueryResult?.isSuccess && - status !== 'error' - ) { - data = prevQueryResult.data - dataUpdatedAt = prevQueryResult.dataUpdatedAt - status = prevQueryResult.status - isPreviousData = true - } // Select data if needed - else if (options.select && typeof state.data !== 'undefined') { + if (options.select && typeof state.data !== 'undefined') { // Memoize select result if ( prevResult && @@ -507,7 +494,9 @@ export class QueryObserver< } else { placeholderData = typeof options.placeholderData === 'function' - ? (options.placeholderData as PlaceholderDataFunction)() + ? (options.placeholderData as PlaceholderDataFunction)( + prevQueryResult?.data as TQueryData | undefined, + ) : options.placeholderData if (options.select && typeof placeholderData !== 'undefined') { try { @@ -548,7 +537,7 @@ export class QueryObserver< isError, isInitialLoading: isLoading && isFetching, data, - dataUpdatedAt, + dataUpdatedAt: state.dataUpdatedAt, error, errorUpdatedAt, failureCount: state.fetchFailureCount, @@ -563,7 +552,6 @@ export class QueryObserver< isLoadingError: isError && state.dataUpdatedAt === 0, isPaused: fetchStatus === 'paused', isPlaceholderData, - isPreviousData, isRefetchError: isError && state.dataUpdatedAt !== 0, isStale: isStale(query, options), refetch: this.refetch, diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 0ff4b0f347..ed117003ef 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -27,7 +27,9 @@ export interface QueryFunctionContext< export type InitialDataFunction = () => T | undefined -export type PlaceholderDataFunction = () => TResult | undefined +export type PlaceholderDataFunction = ( + previousData: TQueryFnData | undefined, +) => TQueryFnData | undefined export type QueryKeyHashFunction = ( queryKey: TQueryKey, @@ -231,11 +233,6 @@ export interface QueryObserverOptions< * Defaults to `false`. */ suspense?: boolean - /** - * Set this to `true` to keep the previous `data` when fetching based on a new query key. - * Defaults to `false`. - */ - keepPreviousData?: boolean /** * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. */ @@ -379,7 +376,6 @@ export interface QueryObserverBaseResult { isInitialLoading: boolean isPaused: boolean isPlaceholderData: boolean - isPreviousData: boolean isRefetchError: boolean isRefetching: boolean isStale: boolean diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx index 821bc0729f..c21bc466ed 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx @@ -85,7 +85,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -118,7 +117,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -168,7 +166,7 @@ describe('useInfiniteQuery', () => { await waitFor(() => expect(noThrow).toBe(true)) }) - it('should keep the previous data when keepPreviousData is set', async () => { + it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: UseInfiniteQueryResult[] = [] @@ -181,9 +179,8 @@ describe('useInfiniteQuery', () => { await sleep(10) return `${pageParam}-${order}` }, - getNextPageParam: () => 1, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', }) @@ -216,28 +213,28 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingNextPage: false, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[1]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[2]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: true, isFetchingNextPage: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[3]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[4]).toMatchObject({ @@ -245,7 +242,7 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingNextPage: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Hook state update expect(states[5]).toMatchObject({ @@ -253,14 +250,14 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingNextPage: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) expect(states[6]).toMatchObject({ data: { pages: ['0-asc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index 2dfd3373ec..66e66866aa 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -84,7 +84,7 @@ describe('useQueries', () => { queries: [ { queryKey: [key1, count], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(10) return count * 2 @@ -92,7 +92,7 @@ describe('useQueries', () => { }, { queryKey: [key2, count], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(35) return count * 5 @@ -125,8 +125,18 @@ describe('useQueries', () => { await waitFor(() => rendered.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 4, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -139,7 +149,7 @@ describe('useQueries', () => { const result = useQueries({ queries: Array.from({ length: count }, (_, i) => ({ queryKey: [key, count, i + 1], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(35 * (i + 1)) return (i + 1) * count * 2 @@ -169,9 +179,24 @@ describe('useQueries', () => { await waitFor(() => rendered.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 6, isPreviousData: false, isFetching: false }, - { status: 'success', data: 12, isPreviousData: false, isFetching: false }, - { status: 'success', data: 18, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 6, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 12, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 18, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -192,7 +217,7 @@ describe('useQueries', () => { await sleep(5) return id * 5 }, - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, } }), }) @@ -226,8 +251,18 @@ describe('useQueries', () => { await waitFor(() => rendered.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - { status: 'success', data: 15, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 15, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -247,7 +282,7 @@ describe('useQueries', () => { await sleep(10) return id * 5 }, - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, } }), }) @@ -287,34 +322,79 @@ describe('useQueries', () => { { status: 'loading', data: undefined, - isPreviousData: false, + isPlaceholderData: false, isFetching: true, }, { status: 'loading', data: undefined, - isPreviousData: false, + isPlaceholderData: false, isFetching: true, }, ]) expect(states[1]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 5, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) expect(states[2]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) expect(states[3]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 5, + isPlaceholderData: false, + isFetching: true, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) expect(states[4]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 5, + isPlaceholderData: false, + isFetching: true, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) expect(states[5]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 5, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) }) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index dcb31aecfa..167c55237d 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -265,7 +265,6 @@ describe('useQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -292,7 +291,6 @@ describe('useQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -349,7 +347,6 @@ describe('useQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -376,7 +373,6 @@ describe('useQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -403,7 +399,6 @@ describe('useQuery', () => { isInitialLoading: false, isLoadingError: true, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -1661,7 +1656,7 @@ describe('useQuery', () => { }) }) - it('should keep the previous data when keepPreviousData is set', async () => { + it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] @@ -1674,7 +1669,7 @@ describe('useQuery', () => { await sleep(10) return count }, - keepPreviousData: true, + placeholderData: (previousData) => previousData, }) states.push(state) @@ -1700,32 +1695,32 @@ describe('useQuery', () => { data: undefined, isFetching: true, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should transition to error state when keepPreviousData is set', async () => { + it('should transition to error state when placeholderData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] @@ -1739,9 +1734,8 @@ describe('useQuery', () => { } return Promise.resolve(count) }, - retry: false, - keepPreviousData: true, + placeholderData: (previousData) => previousData, }) states.push(state) @@ -1750,7 +1744,7 @@ describe('useQuery', () => {

data: {state.data}

error: {state.error?.message}

-

previous data: {state.isPreviousData}

+

placeholder data: {state.isPlaceholderData}

) } @@ -1769,7 +1763,7 @@ describe('useQuery', () => { isFetching: true, status: 'loading', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ @@ -1777,7 +1771,7 @@ describe('useQuery', () => { isFetching: false, status: 'success', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // rerender Page 1 expect(states[2]).toMatchObject({ @@ -1785,7 +1779,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // Hook state update expect(states[3]).toMatchObject({ @@ -1793,7 +1787,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // New data expect(states[4]).toMatchObject({ @@ -1801,7 +1795,7 @@ describe('useQuery', () => { isFetching: false, status: 'success', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // rerender Page 2 expect(states[5]).toMatchObject({ @@ -1809,7 +1803,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // Hook state update again expect(states[6]).toMatchObject({ @@ -1817,19 +1811,19 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // Error expect(states[7]).toMatchObject({ data: undefined, isFetching: false, status: 'error', - isPreviousData: false, + isPlaceholderData: false, }) expect(states[7]?.error).toHaveProperty('message', 'Error test') }) - it('should not show initial data from next query if keepPreviousData is set', async () => { + it('should not show initial data from next query if placeholderData is set', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] @@ -1843,7 +1837,7 @@ describe('useQuery', () => { return count }, initialData: 99, - keepPreviousData: true, + placeholderData: (previousData) => previousData, }) states.push(state) @@ -1878,39 +1872,39 @@ describe('useQuery', () => { data: 99, isFetching: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Hook state update expect(states[3]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // New data expect(states[4]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should keep the previous data on disabled query when keepPreviousData is set', async () => { + it('should keep the previous data on disabled query when placeholderData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] @@ -1924,7 +1918,7 @@ describe('useQuery', () => { return count }, enabled: false, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', }) @@ -1973,46 +1967,46 @@ describe('useQuery', () => { data: undefined, isFetching: false, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetching query expect(states[1]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched query expect(states[2]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[3]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Fetching new query expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Fetched new query expect(states[5]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should keep the previous data on disabled query when keepPreviousData is set and switching query key multiple times', async () => { + it('should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', async () => { const key = queryKey() const states: UseQueryResult[] = [] @@ -2030,7 +2024,7 @@ describe('useQuery', () => { return count }, enabled: false, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', }) @@ -2064,35 +2058,35 @@ describe('useQuery', () => { data: 10, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // State update expect(states[2]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Refetch expect(states[3]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Refetch done expect(states[4]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) @@ -3861,7 +3855,7 @@ describe('useQuery', () => { expect(results[1]).toMatchObject({ data: 1, isFetching: false }) }) - it('should show the correct data when switching keys with initialData, keepPreviousData & staleTime', async () => { + it('should show the correct data when switching keys with initialData, placeholderData & staleTime', async () => { const key = queryKey() const ALL_TODOS = [ @@ -3883,7 +3877,7 @@ describe('useQuery', () => { initialData() { return filter === '' ? initialTodos : undefined }, - keepPreviousData: true, + placeholderData: (previousData) => previousData, staleTime: 5000, }) diff --git a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx index 1546c92db8..841de892ad 100644 --- a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx @@ -95,7 +95,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -128,7 +127,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -182,7 +180,7 @@ describe('useInfiniteQuery', () => { await waitFor(() => expect(noThrow).toBe(true)) }) - it('should keep the previous data when keepPreviousData is set', async () => { + it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: CreateInfiniteQueryResult[] = [] @@ -197,7 +195,7 @@ describe('useInfiniteQuery', () => { }, getNextPageParam: () => 1, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', })) @@ -236,28 +234,28 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingNextPage: false, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[1]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[2]).toMatchObject({ data: { pages: ['0-desc'] }, isFetching: true, isFetchingNextPage: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) expect(states[3]).toMatchObject({ data: { pages: ['0-desc', '1-desc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[4]).toMatchObject({ @@ -265,14 +263,14 @@ describe('useInfiniteQuery', () => { isFetching: true, isFetchingNextPage: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) expect(states[5]).toMatchObject({ data: { pages: ['0-asc'] }, isFetching: false, isFetchingNextPage: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) diff --git a/packages/solid-query/src/__tests__/createQueries.test.tsx b/packages/solid-query/src/__tests__/createQueries.test.tsx index a451068a6f..b7add06216 100644 --- a/packages/solid-query/src/__tests__/createQueries.test.tsx +++ b/packages/solid-query/src/__tests__/createQueries.test.tsx @@ -99,7 +99,7 @@ describe('useQueries', () => { queries: [ { queryKey: [key1, count()], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(10) return count() * 2 @@ -107,7 +107,7 @@ describe('useQueries', () => { }, { queryKey: [key2, count()], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(35) return count() * 5 @@ -147,8 +147,18 @@ describe('useQueries', () => { await waitFor(() => screen.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 4, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 4, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -161,7 +171,7 @@ describe('useQueries', () => { const result = createQueries(() => ({ queries: Array.from({ length: count() }, (_, i) => ({ queryKey: [key, count(), i + 1], - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, queryFn: async () => { await sleep(35 * (i + 1)) return (i + 1) * count() * 2 @@ -197,9 +207,24 @@ describe('useQueries', () => { await waitFor(() => screen.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 6, isPreviousData: false, isFetching: false }, - { status: 'success', data: 12, isPreviousData: false, isFetching: false }, - { status: 'success', data: 18, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 6, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 12, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 18, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -220,7 +245,7 @@ describe('useQueries', () => { await sleep(5) return id() * 5 }, - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, } }), })) @@ -260,8 +285,18 @@ describe('useQueries', () => { await waitFor(() => screen.getByText('isFetching: false')) expect(states[states.length - 1]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - { status: 'success', data: 15, isPreviousData: false, isFetching: false }, + { + status: 'success', + data: 10, + isPlaceholderData: false, + isFetching: false, + }, + { + status: 'success', + data: 15, + isPlaceholderData: false, + isFetching: false, + }, ]) }) @@ -281,7 +316,7 @@ describe('useQueries', () => { await sleep(5) return id * 5 }, - keepPreviousData: true, + placeholderData: (previousData?: number) => previousData, } }), })) @@ -330,39 +365,39 @@ describe('useQueries', () => { { status: 'loading', data: undefined, - isPreviousData: false, + isPlaceholderData: false, isFetching: true, }, { status: 'loading', data: undefined, - isPreviousData: false, + isPlaceholderData: false, isFetching: true, }, ]) expect(states[1]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, + { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, { status: 'loading', data: undefined, - isPreviousData: false, + isPlaceholderData: false, isFetching: true, }, ]) expect(states[2]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, + { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, ]) expect(states[3]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, ]) expect(states[4]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { status: 'success', data: 5, isPlaceholderData: false, isFetching: true }, + { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, ]) expect(states[5]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, + { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, + { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, ]) }) diff --git a/packages/solid-query/src/__tests__/createQuery.test.tsx b/packages/solid-query/src/__tests__/createQuery.test.tsx index 5e708a97d9..043fa5aef0 100644 --- a/packages/solid-query/src/__tests__/createQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createQuery.test.tsx @@ -289,7 +289,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -316,7 +315,6 @@ describe('createQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -378,7 +376,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -405,7 +402,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -432,7 +428,6 @@ describe('createQuery', () => { isInitialLoading: false, isLoadingError: true, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -1597,7 +1592,7 @@ describe('createQuery', () => { }) }) - it('should keep the previous data when keepPreviousData is set', async () => { + it('should keep the previous data when placeholderData is set', async () => { const key = queryKey() const states: CreateQueryResult[] = [] @@ -1610,7 +1605,7 @@ describe('createQuery', () => { await sleep(10) return count() }, - keepPreviousData: true, + placeholderData: (previousData) => previousData, })) createRenderEffect(() => { @@ -1639,32 +1634,32 @@ describe('createQuery', () => { data: undefined, isFetching: true, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should not show initial data from next query if keepPreviousData is set', async () => { + it('should not show initial data from next query if placeholderData is set', async () => { const key = queryKey() const states: DefinedCreateQueryResult[] = [] @@ -1678,7 +1673,7 @@ describe('createQuery', () => { return count() }, initialData: 99, - keepPreviousData: true, + placeholderData: (previousData) => previousData, })) createRenderEffect(() => { @@ -1719,32 +1714,32 @@ describe('createQuery', () => { data: 99, isFetching: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should keep the previous data on disabled query when keepPreviousData is set', async () => { + it('should keep the previous data on disabled query when placeholderData is set to identity function', async () => { const key = queryKey() const states: CreateQueryResult[] = [] @@ -1758,7 +1753,7 @@ describe('createQuery', () => { return count() }, enabled: false, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', })) @@ -1797,46 +1792,46 @@ describe('createQuery', () => { data: undefined, isFetching: false, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetching query expect(states[1]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched query expect(states[2]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[3]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Fetching new query expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Fetched new query expect(states[5]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) - it('should keep the previous data on disabled query when keepPreviousData is set and switching query key multiple times', async () => { + it('should keep the previous data on disabled query when placeholderData is set and switching query key multiple times', async () => { const key = queryKey() const states: CreateQueryResult[] = [] @@ -1854,7 +1849,7 @@ describe('createQuery', () => { return count() }, enabled: false, - keepPreviousData: true, + placeholderData: (previousData) => previousData, notifyOnChangeProps: 'all', })) @@ -1893,28 +1888,28 @@ describe('createQuery', () => { data: 10, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) // Set state expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Refetch expect(states[2]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, - isPreviousData: true, + isPlaceholderData: true, }) // Refetch done expect(states[3]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, - isPreviousData: false, + isPlaceholderData: false, }) }) From c44f34aa50d0c18977f758f0bc567d97e0b49a06 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Wed, 28 Dec 2022 12:49:35 +0100 Subject: [PATCH 02/13] fix: change generic name --- packages/query-core/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index ed117003ef..9de482f372 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -27,9 +27,9 @@ export interface QueryFunctionContext< export type InitialDataFunction = () => T | undefined -export type PlaceholderDataFunction = ( - previousData: TQueryFnData | undefined, -) => TQueryFnData | undefined +export type PlaceholderDataFunction = ( + previousData: TQueryData | undefined, +) => TQueryData | undefined export type QueryKeyHashFunction = ( queryKey: TQueryKey, From 063465cabf7b310c66878ad12f5a71f46810cc19 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Wed, 28 Dec 2022 12:49:55 +0100 Subject: [PATCH 03/13] docs: extend diff in migration guide --- docs/react/guides/migrating-to-react-query-5.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/react/guides/migrating-to-react-query-5.md b/docs/react/guides/migrating-to-react-query-5.md index c260a7c838..8e5be91e0f 100644 --- a/docs/react/guides/migrating-to-react-query-5.md +++ b/docs/react/guides/migrating-to-react-query-5.md @@ -170,7 +170,11 @@ To achieve the same functionality as `keepPreviousData`, we have added previous Therefore you just need to provide an identity function to `placeholderData` ```diff -useQuery({ +const { + data, +- isPreviousData, ++ isPlaceholderData, +} = useQuery({ queryKey, queryFn, - keepPreviousData: true, From 4e6af88747871689988506dd1323d271c976b104 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Thu, 29 Dec 2022 01:00:47 +0100 Subject: [PATCH 04/13] tests: remove irrelevant tests --- .../src/__tests__/useQueries.test.tsx | 325 ------------------ .../src/__tests__/createQueries.test.tsx | 314 ----------------- 2 files changed, 639 deletions(-) diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index 66e66866aa..f10f22decb 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -73,331 +73,6 @@ describe('useQueries', () => { expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) - it('should keep previous data if amount of queries is the same', async () => { - const key1 = queryKey() - const key2 = queryKey() - const states: UseQueryResult[][] = [] - - function Page() { - const [count, setCount] = React.useState(1) - const result = useQueries({ - queries: [ - { - queryKey: [key1, count], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(10) - return count * 2 - }, - }, - { - queryKey: [key2, count], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(35) - return count * 5 - }, - }, - ], - }) - states.push(result) - - const isFetching = result.some((r) => r.isFetching) - - return ( -
-
- data1: {String(result[0].data ?? 'null')}, data2:{' '} - {String(result[1].data ?? 'null')} -
-
isFetching: {String(isFetching)}
- -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('data1: 2, data2: 5')) - fireEvent.click(rendered.getByRole('button', { name: /inc/i })) - - await waitFor(() => rendered.getByText('data1: 4, data2: 10')) - await waitFor(() => rendered.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 4, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should keep previous data for variable amounts of useQueries', async () => { - const key = queryKey() - const states: UseQueryResult[][] = [] - - function Page() { - const [count, setCount] = React.useState(2) - const result = useQueries({ - queries: Array.from({ length: count }, (_, i) => ({ - queryKey: [key, count, i + 1], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(35 * (i + 1)) - return (i + 1) * count * 2 - }, - })), - }) - - states.push(result) - - const isFetching = result.some((r) => r.isFetching) - - return ( -
-
data: {result.map((it) => it.data).join(',')}
-
isFetching: {String(isFetching)}
- -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('data: 4,8')) - fireEvent.click(rendered.getByRole('button', { name: /inc/i })) - - await waitFor(() => rendered.getByText('data: 6,12,18')) - await waitFor(() => rendered.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 6, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 12, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 18, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should keep previous data when switching between queries', async () => { - const key = queryKey() - const states: UseQueryResult[][] = [] - - function Page() { - const [series1, setSeries1] = React.useState(1) - const [series2, setSeries2] = React.useState(2) - const ids = [series1, series2] - - const result = useQueries({ - queries: ids.map((id) => { - return { - queryKey: [key, id], - queryFn: async () => { - await sleep(5) - return id * 5 - }, - placeholderData: (previousData?: number) => previousData, - } - }), - }) - - states.push(result) - - const isFetching = result.some((r) => r.isFetching) - - return ( -
-
- data1: {String(result[0]?.data ?? 'null')}, data2:{' '} - {String(result[1]?.data ?? 'null')} -
-
isFetching: {String(isFetching)}
- - -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('data1: 5, data2: 10')) - fireEvent.click(rendered.getByRole('button', { name: /setSeries2/i })) - - await waitFor(() => rendered.getByText('data1: 5, data2: 15')) - fireEvent.click(rendered.getByRole('button', { name: /setSeries1/i })) - - await waitFor(() => rendered.getByText('data1: 10, data2: 15')) - await waitFor(() => rendered.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 15, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should not go to infinite render loop with previous data when toggling queries', async () => { - const key = queryKey() - const states: UseQueryResult[][] = [] - - function Page() { - const [enableId1, setEnableId1] = React.useState(true) - const ids = enableId1 ? [1, 2] : [2] - - const result = useQueries({ - queries: ids.map((id) => { - return { - queryKey: [key, id], - queryFn: async () => { - await sleep(10) - return id * 5 - }, - placeholderData: (previousData?: number) => previousData, - } - }), - }) - - states.push(result) - - const isFetching = result.some((r) => r.isFetching) - - return ( -
-
- data1: {String(result[0]?.data ?? 'null')}, data2:{' '} - {String(result[1]?.data ?? 'null')} -
-
isFetching: {String(isFetching)}
- - -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('data1: 5, data2: 10')) - fireEvent.click(rendered.getByRole('button', { name: /set1Disabled/i })) - - await waitFor(() => rendered.getByText('data1: 10, data2: null')) - await waitFor(() => rendered.getByText('isFetching: false')) - fireEvent.click(rendered.getByRole('button', { name: /set2Enabled/i })) - - await waitFor(() => rendered.getByText('data1: 5, data2: 10')) - await waitFor(() => rendered.getByText('isFetching: false')) - - await waitFor(() => expect(states.length).toBe(6)) - - expect(states[0]).toMatchObject([ - { - status: 'loading', - data: undefined, - isPlaceholderData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPlaceholderData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { - status: 'success', - data: 5, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - expect(states[2]).toMatchObject([ - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - expect(states[3]).toMatchObject([ - { - status: 'success', - data: 5, - isPlaceholderData: false, - isFetching: true, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - expect(states[4]).toMatchObject([ - { - status: 'success', - data: 5, - isPlaceholderData: false, - isFetching: true, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - expect(states[5]).toMatchObject([ - { - status: 'success', - data: 5, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - it('handles type parameter - tuple of tuples', async () => { const key1 = queryKey() const key2 = queryKey() diff --git a/packages/solid-query/src/__tests__/createQueries.test.tsx b/packages/solid-query/src/__tests__/createQueries.test.tsx index b7add06216..490ca37534 100644 --- a/packages/solid-query/src/__tests__/createQueries.test.tsx +++ b/packages/solid-query/src/__tests__/createQueries.test.tsx @@ -5,7 +5,6 @@ import * as QueriesObserverModule from '../../../query-core/src/queriesObserver' import type { QueryFunctionContext, QueryKey } from '@tanstack/query-core' import { createContext, - createMemo, createRenderEffect, createSignal, ErrorBoundary, @@ -88,319 +87,6 @@ describe('useQueries', () => { expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) - it('should keep previous data if amount of queries is the same', async () => { - const key1 = queryKey() - const key2 = queryKey() - const states: CreateQueryResult[][] = [] - - function Page() { - const [count, setCount] = createSignal(1) - const result = createQueries(() => ({ - queries: [ - { - queryKey: [key1, count()], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(10) - return count() * 2 - }, - }, - { - queryKey: [key2, count()], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(35) - return count() * 5 - }, - }, - ], - })) - - createRenderEffect(() => { - states.push([...result]) - }) - - const isFetching = createMemo(() => result.some((r) => r.isFetching)) - - return ( -
-
- data1: {String(result[0].data ?? 'null')}, data2:{' '} - {String(result[1].data ?? 'null')} -
-
isFetching: {String(isFetching())}
- -
- ) - } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('data1: 2, data2: 5')) - fireEvent.click(screen.getByRole('button', { name: /inc/i })) - - await waitFor(() => screen.getByText('data1: 4, data2: 10')) - await waitFor(() => screen.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 4, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should keep previous data for variable amounts of useQueries', async () => { - const key = queryKey() - const states: CreateQueryResult[][] = [] - - function Page() { - const [count, setCount] = createSignal(2) - const result = createQueries(() => ({ - queries: Array.from({ length: count() }, (_, i) => ({ - queryKey: [key, count(), i + 1], - placeholderData: (previousData?: number) => previousData, - queryFn: async () => { - await sleep(35 * (i + 1)) - return (i + 1) * count() * 2 - }, - })), - })) - - createRenderEffect(() => { - states.push([...result]) - }) - - const isFetching = createMemo(() => result.some((r) => r.isFetching)) - - return ( -
-
data: {result.map((it) => it.data).join(',')}
-
isFetching: {String(isFetching())}
- -
- ) - } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('data: 4,8')) - fireEvent.click(screen.getByRole('button', { name: /inc/i })) - - await waitFor(() => screen.getByText('data: 6,12,18')) - await waitFor(() => screen.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 6, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 12, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 18, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should keep previous data when switching between queries', async () => { - const key = queryKey() - const states: CreateQueryResult[][] = [] - - function Page() { - const [series1, setSeries1] = createSignal(1) - const [series2, setSeries2] = createSignal(2) - const ids = [series1, series2] - - const result = createQueries(() => ({ - queries: ids.map((id) => { - return { - queryKey: [key, id()], - queryFn: async () => { - await sleep(5) - return id() * 5 - }, - placeholderData: (previousData?: number) => previousData, - } - }), - })) - - createRenderEffect(() => { - states.push([...result]) - }) - - const isFetching = createMemo(() => result.some((r) => r.isFetching)) - - return ( -
-
- data1: {String(result[0]?.data ?? 'null')}, data2:{' '} - {String(result[1]?.data ?? 'null')} -
-
isFetching: {String(isFetching())}
- - -
- ) - } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('data1: 5, data2: 10')) - fireEvent.click(screen.getByRole('button', { name: /setSeries2/i })) - - await waitFor(() => screen.getByText('data1: 5, data2: 15')) - fireEvent.click(screen.getByRole('button', { name: /setSeries1/i })) - - await waitFor(() => screen.getByText('data1: 10, data2: 15')) - await waitFor(() => screen.getByText('isFetching: false')) - - expect(states[states.length - 1]).toMatchObject([ - { - status: 'success', - data: 10, - isPlaceholderData: false, - isFetching: false, - }, - { - status: 'success', - data: 15, - isPlaceholderData: false, - isFetching: false, - }, - ]) - }) - - it('should not go to infinite render loop with previous data when toggling queries', async () => { - const key = queryKey() - const states: CreateQueryResult[][] = [] - - function Page() { - const [enableId1, setEnableId1] = createSignal(true) - const ids = createMemo(() => (enableId1() ? [1, 2] : [2])) - - const result = createQueries(() => ({ - queries: ids().map((id) => { - return { - queryKey: [key, id], - queryFn: async () => { - await sleep(5) - return id * 5 - }, - placeholderData: (previousData?: number) => previousData, - } - }), - })) - - createRenderEffect(() => { - states.push([...result]) - }) - - const text = createMemo(() => { - return result - .map((r, idx) => `data${idx + 1}: ${r.data ?? 'null'}`) - .join(' ') - }) - - const isFetching = createMemo(() => result.some((r) => r.isFetching)) - - return ( -
-
{text()}
-
isFetching: {String(isFetching())}
- - -
- ) - } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('data1: 5 data2: 10')) - fireEvent.click(screen.getByRole('button', { name: /set1Disabled/i })) - - await waitFor(() => screen.getByText('data1: 10')) - await waitFor(() => screen.getByText('isFetching: false')) - fireEvent.click(screen.getByRole('button', { name: /set2Enabled/i })) - - await waitFor(() => screen.getByText('data1: 5 data2: 10')) - await waitFor(() => screen.getByText('isFetching: false')) - - await waitFor(() => expect(states.length).toBe(6)) - - expect(states[0]).toMatchObject([ - { - status: 'loading', - data: undefined, - isPlaceholderData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPlaceholderData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPlaceholderData: false, - isFetching: true, - }, - ]) - expect(states[2]).toMatchObject([ - { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, - { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, - ]) - expect(states[3]).toMatchObject([ - { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 5, isPlaceholderData: false, isFetching: true }, - { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, - ]) - expect(states[5]).toMatchObject([ - { status: 'success', data: 5, isPlaceholderData: false, isFetching: false }, - { status: 'success', data: 10, isPlaceholderData: false, isFetching: false }, - ]) - }) - it('handles type parameter - tuple of tuples', async () => { const key1 = queryKey() const key2 = queryKey() From 16f90397d994f4c8aa523f3ce80e7963d89ccea1 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Fri, 30 Dec 2022 16:52:19 +0100 Subject: [PATCH 05/13] fix: useQueries placeholder data parameter removed --- packages/query-core/src/types.ts | 4 ++ packages/react-query/src/useQueries.ts | 13 +++++- packages/solid-query/src/createQueries.ts | 13 +++++- packages/vue-query/src/useQueries.ts | 51 ++++++++++++++++------- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 9de482f372..ff2243e5d7 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -31,6 +31,10 @@ export type PlaceholderDataFunction = ( previousData: TQueryData | undefined, ) => TQueryData | undefined +export type QueriesPlaceholderDataFunction = () => + | TQueryData + | undefined + export type QueryKeyHashFunction = ( queryKey: TQueryKey, ) => string diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 281b5dd187..38ef3408a5 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -1,7 +1,11 @@ import * as React from 'react' import { useSyncExternalStore } from './useSyncExternalStore' -import type { QueryKey, QueryFunction } from '@tanstack/query-core' +import type { + QueryKey, + QueryFunction, + QueriesPlaceholderDataFunction, +} from '@tanstack/query-core' import { notifyManager, QueriesObserver } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import type { UseQueryOptions, UseQueryResult } from './types' @@ -26,7 +30,12 @@ type UseQueryOptionsForUseQueries< TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = Omit, 'context'> +> = Omit< + UseQueryOptions, + 'context' | 'placeholderData' +> & { + placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction +} // Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20 diff --git a/packages/solid-query/src/createQueries.ts b/packages/solid-query/src/createQueries.ts index d245bdcc1a..bb54964039 100644 --- a/packages/solid-query/src/createQueries.ts +++ b/packages/solid-query/src/createQueries.ts @@ -1,4 +1,8 @@ -import type { QueryFunction, QueryKey } from '@tanstack/query-core' +import type { + QueriesPlaceholderDataFunction, + QueryFunction, + QueryKey, +} from '@tanstack/query-core' import { notifyManager, QueriesObserver } from '@tanstack/query-core' import { createComputed, onCleanup, onMount } from 'solid-js' import { createStore, unwrap } from 'solid-js/store' @@ -12,7 +16,12 @@ type CreateQueryOptionsForCreateQueries< TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = Omit, 'context'> +> = Omit< + SolidQueryOptions, + 'context' | 'placeholderData' +> & { + placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction +} // Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20 diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index ab40ebcb09..bf4c81620e 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -1,5 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { QueriesObserver } from '@tanstack/query-core' +import type { + QueriesPlaceholderDataFunction, + QueryKey, +} from '@tanstack/query-core' import { computed, onScopeDispose, @@ -17,6 +21,20 @@ import { cloneDeepUnref } from './utils' import type { UseQueryOptions } from './useQuery' import type { QueryClient } from './queryClient' +// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. +// - `context` is omitted as it is passed as a root-level option to `useQueries` instead. +type UseQueryOptionsForUseQueries< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + UseQueryOptions, + 'context' | 'placeholderData' +> & { + placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction +} + // Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20 @@ -27,28 +45,33 @@ type GetOptions = error?: infer TError data: infer TData } - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : T extends { queryFnData: infer TQueryFnData; error?: infer TError } - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : T extends { data: infer TData; error?: infer TError } - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] T extends [infer TQueryFnData, infer TError, infer TData] - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData, infer TError] - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : T extends [infer TQueryFnData] - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided T extends { queryFn?: QueryFunction select: (data: any) => infer TData } - ? UseQueryOptions + ? UseQueryOptionsForUseQueries : T extends { queryFn?: QueryFunction } - ? UseQueryOptions + ? UseQueryOptionsForUseQueries< + TQueryFnData, + Error, + TQueryFnData, + TQueryKey + > : // Fallback - UseQueryOptions + UseQueryOptionsForUseQueries type GetResults = // Part 1: responsible for mapping explicit type parameter to function result, if object @@ -84,7 +107,7 @@ export type UseQueriesOptions< Result extends any[] = [], Depth extends ReadonlyArray = [], > = Depth['length'] extends MAXIMUM_DEPTH - ? UseQueryOptions[] + ? UseQueryOptionsForUseQueries[] : T extends [] ? [] : T extends [infer Head] @@ -95,15 +118,15 @@ export type UseQueriesOptions< ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! // use this to infer the param types in the case of Array.map() argument - T extends UseQueryOptions< + T extends UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData, infer TQueryKey >[] - ? UseQueryOptions[] + ? UseQueryOptionsForUseQueries[] : // Fallback - UseQueryOptions[] + UseQueryOptionsForUseQueries[] /** * UseQueriesResults reducer recursively maps type param to results @@ -120,7 +143,7 @@ export type UseQueriesResults< ? [...Result, GetResults] : T extends [infer Head, ...infer Tail] ? UseQueriesResults<[...Tail], [...Result, GetResults], [...Depth, 1]> - : T extends UseQueryOptions< + : T extends UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData, From edf3b73dbe4738d0b0cd43faf0690ea01aabe907 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Fri, 30 Dec 2022 18:17:07 +0100 Subject: [PATCH 06/13] feat: keepPreviousData identity function --- docs/react/guides/migrating-to-react-query-5.md | 4 ++-- docs/react/guides/paginated-queries.md | 4 ++-- examples/react/pagination/pages/index.js | 3 ++- packages/query-core/src/index.ts | 2 +- packages/query-core/src/types.ts | 4 +++- packages/query-core/src/utils.ts | 6 ++++++ .../src/__tests__/useInfiniteQuery.test.tsx | 4 ++-- .../react-query/src/__tests__/useQuery.test.tsx | 14 +++++++------- .../src/__tests__/createInfiniteQuery.test.tsx | 4 ++-- .../solid-query/src/__tests__/createQuery.test.tsx | 10 +++++----- 10 files changed, 32 insertions(+), 23 deletions(-) diff --git a/docs/react/guides/migrating-to-react-query-5.md b/docs/react/guides/migrating-to-react-query-5.md index 8e5be91e0f..ea4efbf07a 100644 --- a/docs/react/guides/migrating-to-react-query-5.md +++ b/docs/react/guides/migrating-to-react-query-5.md @@ -167,7 +167,7 @@ Since the only supported syntax now is the object syntax, this rule is no longer We have removed the `keepPreviousData` option and `isPreviousData` flag as they were doing mostly the same thing as `placeholderData` and `isPlaceholderData` flag. To achieve the same functionality as `keepPreviousData`, we have added previous query `data` as an argument to `placeholderData` function. -Therefore you just need to provide an identity function to `placeholderData` +Therefore you just need to provide an identity function to `placeholderData` or use `keepPreviousData` function returned from Tanstack Query. ```diff const { @@ -178,7 +178,7 @@ const { queryKey, queryFn, - keepPreviousData: true, -+ placeholderData: (previousData) => previousData ++ placeholderData: keepPreviousData }); ``` diff --git a/docs/react/guides/paginated-queries.md b/docs/react/guides/paginated-queries.md index 965361b613..bee69f3e4e 100644 --- a/docs/react/guides/paginated-queries.md +++ b/docs/react/guides/paginated-queries.md @@ -22,7 +22,7 @@ This experience is not optimal and unfortunately is how many tools today insist ## Better Paginated Queries with `placeholderData` -Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `placeholderData` to `(previousData) => previousData` we get a few new things: +Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `placeholderData` to `(previousData) => previousData` or `keepPreviousData` function exported from TanStack Query, we get a few new things: - **The data from the last successful fetch available while new data is being requested, even though the query key has changed**. - When the new data arrives, the previous `data` is seamlessly swapped to show the new data. @@ -45,7 +45,7 @@ function Todos() { } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, }) return ( diff --git a/examples/react/pagination/pages/index.js b/examples/react/pagination/pages/index.js index 1ade0de83a..41693acfff 100644 --- a/examples/react/pagination/pages/index.js +++ b/examples/react/pagination/pages/index.js @@ -5,6 +5,7 @@ import { useQueryClient, QueryClient, QueryClientProvider, + keepPreviousData, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -30,7 +31,7 @@ function Example() { const { status, data, error, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, staleTime: 5000, }) diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index d2bee42dd8..a15458e179 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -11,7 +11,7 @@ export { MutationObserver } from './mutationObserver' export { notifyManager } from './notifyManager' export { focusManager } from './focusManager' export { onlineManager } from './onlineManager' -export { hashQueryKey, replaceEqualDeep, isError, isServer } from './utils' +export { hashQueryKey, replaceEqualDeep, isError, isServer, keepPreviousData } from './utils' export type { MutationFilters, QueryFilters, Updater } from './utils' export { isCancelledError } from './retryer' export { dehydrate, hydrate } from './hydration' diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index ff2243e5d7..e1f1612082 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -27,6 +27,8 @@ export interface QueryFunctionContext< export type InitialDataFunction = () => T | undefined +type NonFunctionGuard = T extends Function ? never : T + export type PlaceholderDataFunction = ( previousData: TQueryData | undefined, ) => TQueryData | undefined @@ -240,7 +242,7 @@ export interface QueryObserverOptions< /** * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. */ - placeholderData?: TQueryData | PlaceholderDataFunction + placeholderData?: NonFunctionGuard | PlaceholderDataFunction> _optimisticResults?: 'optimistic' | 'isRestoring' } diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index 5adf603245..0f16c10b20 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -357,3 +357,9 @@ export function replaceData< } return data } + +export function keepPreviousData( + previousData: T | undefined, +): T | undefined { + return previousData +} diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx index c21bc466ed..2c994dd5db 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx @@ -13,7 +13,7 @@ import type { QueryFunctionContext, UseInfiniteQueryResult, } from '..' -import { QueryCache, useInfiniteQuery } from '..' +import { QueryCache, useInfiniteQuery, keepPreviousData } from '..' interface Result { items: number[] @@ -180,7 +180,7 @@ describe('useInfiniteQuery', () => { return `${pageParam}-${order}` }, getNextPageParam: () => 1, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 167c55237d..35b90df1ca 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -20,7 +20,7 @@ import type { UseQueryOptions, UseQueryResult, } from '..' -import { QueryCache, useQuery } from '..' +import { QueryCache, useQuery, keepPreviousData } from '..' import { ErrorBoundary } from 'react-error-boundary' describe('useQuery', () => { @@ -1669,7 +1669,7 @@ describe('useQuery', () => { await sleep(10) return count }, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, }) states.push(state) @@ -1735,7 +1735,7 @@ describe('useQuery', () => { return Promise.resolve(count) }, retry: false, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, }) states.push(state) @@ -1837,7 +1837,7 @@ describe('useQuery', () => { return count }, initialData: 99, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, }) states.push(state) @@ -1918,7 +1918,7 @@ describe('useQuery', () => { return count }, enabled: false, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) @@ -2024,7 +2024,7 @@ describe('useQuery', () => { return count }, enabled: false, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) @@ -3877,7 +3877,7 @@ describe('useQuery', () => { initialData() { return filter === '' ? initialTodos : undefined }, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, staleTime: 5000, }) diff --git a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx index 841de892ad..789fb63c7f 100644 --- a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx @@ -16,7 +16,7 @@ import type { InfiniteData, QueryFunctionContext, } from '..' -import { createInfiniteQuery, QueryCache, QueryClientProvider } from '..' +import { createInfiniteQuery, QueryCache, QueryClientProvider, keepPreviousData } from '..' import { Blink, queryKey, setActTimeout } from './utils' interface Result { @@ -195,7 +195,7 @@ describe('useInfiniteQuery', () => { }, getNextPageParam: () => 1, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) diff --git a/packages/solid-query/src/__tests__/createQuery.test.tsx b/packages/solid-query/src/__tests__/createQuery.test.tsx index 043fa5aef0..848b43eb0a 100644 --- a/packages/solid-query/src/__tests__/createQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createQuery.test.tsx @@ -17,7 +17,7 @@ import type { DefinedCreateQueryResult, QueryFunction, } from '..' -import { createQuery, QueryCache, QueryClientProvider } from '..' +import { createQuery, QueryCache, QueryClientProvider, keepPreviousData } from '..' import { Blink, createQueryClient, @@ -1605,7 +1605,7 @@ describe('createQuery', () => { await sleep(10) return count() }, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, })) createRenderEffect(() => { @@ -1673,7 +1673,7 @@ describe('createQuery', () => { return count() }, initialData: 99, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, })) createRenderEffect(() => { @@ -1753,7 +1753,7 @@ describe('createQuery', () => { return count() }, enabled: false, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) @@ -1849,7 +1849,7 @@ describe('createQuery', () => { return count() }, enabled: false, - placeholderData: (previousData) => previousData, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) From b908492b869dd5b911909a1a802f92d6be23cd3a Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Fri, 30 Dec 2022 18:34:47 +0100 Subject: [PATCH 07/13] fix: formatting --- .../solid-query/src/__tests__/createInfiniteQuery.test.tsx | 7 ++++++- packages/solid-query/src/__tests__/createQuery.test.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx index 789fb63c7f..ee88ae121d 100644 --- a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx @@ -16,7 +16,12 @@ import type { InfiniteData, QueryFunctionContext, } from '..' -import { createInfiniteQuery, QueryCache, QueryClientProvider, keepPreviousData } from '..' +import { + createInfiniteQuery, + QueryCache, + QueryClientProvider, + keepPreviousData, +} from '..' import { Blink, queryKey, setActTimeout } from './utils' interface Result { diff --git a/packages/solid-query/src/__tests__/createQuery.test.tsx b/packages/solid-query/src/__tests__/createQuery.test.tsx index 848b43eb0a..0d0e220cb0 100644 --- a/packages/solid-query/src/__tests__/createQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createQuery.test.tsx @@ -17,7 +17,12 @@ import type { DefinedCreateQueryResult, QueryFunction, } from '..' -import { createQuery, QueryCache, QueryClientProvider, keepPreviousData } from '..' +import { + createQuery, + QueryCache, + QueryClientProvider, + keepPreviousData, +} from '..' import { Blink, createQueryClient, From be288627a3182bd67631cbd2a6756a576c32c070 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Fri, 30 Dec 2022 18:35:42 +0100 Subject: [PATCH 08/13] fix: ts 4.7 error --- packages/query-core/src/queryObserver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 31dc3acc32..303e88ed08 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -494,9 +494,9 @@ export class QueryObserver< } else { placeholderData = typeof options.placeholderData === 'function' - ? (options.placeholderData as PlaceholderDataFunction)( - prevQueryResult?.data as TQueryData | undefined, - ) + ? ( + options.placeholderData as unknown as PlaceholderDataFunction + )(prevQueryResult?.data as TQueryData | undefined) : options.placeholderData if (options.select && typeof placeholderData !== 'undefined') { try { From b397ca4554806fc131fbfd41d078bdb252450418 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Fri, 30 Dec 2022 18:40:03 +0100 Subject: [PATCH 09/13] fix: formatting --- packages/query-core/src/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index e1f1612082..8312473fd4 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -242,7 +242,9 @@ export interface QueryObserverOptions< /** * If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided. */ - placeholderData?: NonFunctionGuard | PlaceholderDataFunction> + placeholderData?: + | NonFunctionGuard + | PlaceholderDataFunction> _optimisticResults?: 'optimistic' | 'isRestoring' } From e7102a21544993854bbba79cead1bea81509fbde Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 2 Jan 2023 23:46:07 +0100 Subject: [PATCH 10/13] docs: add a note about useQueries --- docs/react/guides/migrating-to-react-query-5.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/react/guides/migrating-to-react-query-5.md b/docs/react/guides/migrating-to-react-query-5.md index ea4efbf07a..f4958b41ba 100644 --- a/docs/react/guides/migrating-to-react-query-5.md +++ b/docs/react/guides/migrating-to-react-query-5.md @@ -169,6 +169,8 @@ We have removed the `keepPreviousData` option and `isPreviousData` flag as they To achieve the same functionality as `keepPreviousData`, we have added previous query `data` as an argument to `placeholderData` function. Therefore you just need to provide an identity function to `placeholderData` or use `keepPreviousData` function returned from Tanstack Query. +> A note here is that `useQueries` would not receive `previousData` in the `placeholderData` function as argument. This is due to a dynamic nature of queries passed in the array, which may lead to a different shape of result from placeholder and queryFn. + ```diff const { data, From 25c99cefb7c6af8f0595b52b0636c5ca96233b8d Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 2 Jan 2023 23:50:51 +0100 Subject: [PATCH 11/13] chore: fix formatting --- packages/query-core/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index a15458e179..3c705d286a 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -11,7 +11,13 @@ export { MutationObserver } from './mutationObserver' export { notifyManager } from './notifyManager' export { focusManager } from './focusManager' export { onlineManager } from './onlineManager' -export { hashQueryKey, replaceEqualDeep, isError, isServer, keepPreviousData } from './utils' +export { + hashQueryKey, + replaceEqualDeep, + isError, + isServer, + keepPreviousData, +} from './utils' export type { MutationFilters, QueryFilters, Updater } from './utils' export { isCancelledError } from './retryer' export { dehydrate, hydrate } from './hydration' From b5df75842b086c2864c5bb3505f1a22aec6cdb1e Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Mon, 9 Jan 2023 23:59:16 +0100 Subject: [PATCH 12/13] fix: formatting --- packages/vue-query/src/useQueries.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index bf4c81620e..de86b08be9 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -64,12 +64,7 @@ type GetOptions = } ? UseQueryOptionsForUseQueries : T extends { queryFn?: QueryFunction } - ? UseQueryOptionsForUseQueries< - TQueryFnData, - Error, - TQueryFnData, - TQueryKey - > + ? UseQueryOptionsForUseQueries : // Fallback UseQueryOptionsForUseQueries From d6058fe18f91a9c62b13ec218ba685b4c89c2a68 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 13 Jan 2023 14:10:05 +0100 Subject: [PATCH 13/13] Apply suggestions from code review --- docs/react/guides/migrating-to-v5.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/react/guides/migrating-to-v5.md b/docs/react/guides/migrating-to-v5.md index 460a37ba84..8d674534bb 100644 --- a/docs/react/guides/migrating-to-v5.md +++ b/docs/react/guides/migrating-to-v5.md @@ -186,8 +186,8 @@ const { There are some caveats to this change however, which you must be aware of: -- `placeholderData` will always put you into `success` state, while `keepPreviousData` will give you the status of the previous query. That status could be `error` if we have data fetched successfully and then got a background refetch error. However, the error itself is not shared. This doesn't seem quite right in any case. Therefore we decided to stick with behavior of `placeholderData`. -- `keepPreviousData` will give you the `dataUpdatedAt` timestamp of the previous data, while with `placeholderData`, `dataUpdatedAt` will stay at `0`. This might be annoying if you want to show that timestamp continuously on screen. However you might get around it with `useEffect`. +- `placeholderData` will always put you into `success` state, while `keepPreviousData` gave you the status of the previous query. That status could be `error` if we have data fetched successfully and then got a background refetch error. However, the error itself was not shared, so we decided to stick with behavior of `placeholderData`. +- `keepPreviousData` gave you the `dataUpdatedAt` timestamp of the previous data, while with `placeholderData`, `dataUpdatedAt` will stay at `0`. This might be annoying if you want to show that timestamp continuously on screen. However you might get around it with `useEffect`. ```ts const [updatedAt, setUpdatedAt] = useState(0)