diff --git a/docs/react/guides/migrating-to-v5.md b/docs/react/guides/migrating-to-v5.md index 3e07451f04..8d674534bb 100644 --- a/docs/react/guides/migrating-to-v5.md +++ b/docs/react/guides/migrating-to-v5.md @@ -1,5 +1,5 @@ --- -id: migrating-to-v5 +id: migrating-to-react-query-5 title: Migrating to TanStack Query v5 --- @@ -150,11 +150,11 @@ 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 + }, }) ``` @@ -162,6 +162,48 @@ useQuery({ 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` 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, +- isPreviousData, ++ isPlaceholderData, +} = useQuery({ + queryKey, + queryFn, +- keepPreviousData: true, ++ placeholderData: keepPreviousData +}); +``` + +There are some caveats to this change however, which you must be aware of: + +- `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) + + const { data, dataUpdatedAt } = useQuery({ + queryKey: ['projects', page], + queryFn: () => fetchProjects(page), + }) + + useEffect(() => { + if (dataUpdatedAt > updatedAt) { + setUpdatedAt(dataUpdatedAt) + } + }, [dataUpdatedAt]) + ``` + ### Window focus refetching no longer listens to the `focus` event The `visibilitychange` event is used exclusively now. This is possible because we only support browsers that support the `visibilitychange` event. This fixes a bunch of issues [as listed here](https://github.com/TanStack/query/pull/4805). diff --git a/docs/react/guides/paginated-queries.md b/docs/react/guides/paginated-queries.md index b1bbe85f4d..bee69f3e4e 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` 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. -- `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: keepPreviousData, }) 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..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' @@ -27,22 +28,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: keepPreviousData, 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 +79,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/index.ts b/packages/query-core/src/index.ts index d2bee42dd8..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 } 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/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..303e88ed08 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 unknown 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..8312473fd4 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -27,7 +27,15 @@ export interface QueryFunctionContext< export type InitialDataFunction = () => T | undefined -export type PlaceholderDataFunction = () => TResult | undefined +type NonFunctionGuard = T extends Function ? never : T + +export type PlaceholderDataFunction = ( + previousData: TQueryData | undefined, +) => TQueryData | undefined + +export type QueriesPlaceholderDataFunction = () => + | TQueryData + | undefined export type QueryKeyHashFunction = ( queryKey: TQueryKey, @@ -231,15 +239,12 @@ 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. */ - placeholderData?: TQueryData | PlaceholderDataFunction + placeholderData?: + | NonFunctionGuard + | PlaceholderDataFunction> _optimisticResults?: 'optimistic' | 'isRestoring' } @@ -379,7 +384,6 @@ export interface QueryObserverBaseResult { isInitialLoading: boolean isPaused: boolean isPlaceholderData: boolean - isPreviousData: boolean isRefetchError: boolean isRefetching: boolean isStale: boolean 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 821bc0729f..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[] @@ -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: keepPreviousData, 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..f10f22decb 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -73,251 +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], - keepPreviousData: true, - queryFn: async () => { - await sleep(10) - return count * 2 - }, - }, - { - queryKey: [key2, count], - keepPreviousData: true, - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: 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], - keepPreviousData: true, - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 12, isPreviousData: false, isFetching: false }, - { status: 'success', data: 18, isPreviousData: 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 - }, - keepPreviousData: true, - } - }), - }) - - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 15, isPreviousData: 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 - }, - keepPreviousData: true, - } - }), - }) - - 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, - isPreviousData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[2]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[3]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[5]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - }) - it('handles type parameter - tuple of tuples', async () => { const key1 = queryKey() const key2 = queryKey() diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 96961399bd..96ac6a1de9 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', () => { @@ -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, @@ -1664,7 +1659,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[] = [] @@ -1677,7 +1672,7 @@ describe('useQuery', () => { await sleep(10) return count }, - keepPreviousData: true, + placeholderData: keepPreviousData, }) states.push(state) @@ -1703,32 +1698,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[] = [] @@ -1742,9 +1737,8 @@ describe('useQuery', () => { } return Promise.resolve(count) }, - retry: false, - keepPreviousData: true, + placeholderData: keepPreviousData, }) states.push(state) @@ -1753,7 +1747,7 @@ describe('useQuery', () => {

data: {state.data}

error: {state.error?.message}

-

previous data: {state.isPreviousData}

+

placeholder data: {state.isPlaceholderData}

) } @@ -1772,7 +1766,7 @@ describe('useQuery', () => { isFetching: true, status: 'loading', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // Fetched expect(states[1]).toMatchObject({ @@ -1780,7 +1774,7 @@ describe('useQuery', () => { isFetching: false, status: 'success', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // rerender Page 1 expect(states[2]).toMatchObject({ @@ -1788,7 +1782,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // Hook state update expect(states[3]).toMatchObject({ @@ -1796,7 +1790,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // New data expect(states[4]).toMatchObject({ @@ -1804,7 +1798,7 @@ describe('useQuery', () => { isFetching: false, status: 'success', error: null, - isPreviousData: false, + isPlaceholderData: false, }) // rerender Page 2 expect(states[5]).toMatchObject({ @@ -1812,7 +1806,7 @@ describe('useQuery', () => { isFetching: true, status: 'success', error: null, - isPreviousData: true, + isPlaceholderData: true, }) // Hook state update again expect(states[6]).toMatchObject({ @@ -1820,19 +1814,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[] = [] @@ -1846,7 +1840,7 @@ describe('useQuery', () => { return count }, initialData: 99, - keepPreviousData: true, + placeholderData: keepPreviousData, }) states.push(state) @@ -1881,39 +1875,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[] = [] @@ -1927,7 +1921,7 @@ describe('useQuery', () => { return count }, enabled: false, - keepPreviousData: true, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) @@ -1976,46 +1970,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[] = [] @@ -2033,7 +2027,7 @@ describe('useQuery', () => { return count }, enabled: false, - keepPreviousData: true, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', }) @@ -2067,35 +2061,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, }) }) @@ -3864,7 +3858,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 = [ @@ -3886,7 +3880,7 @@ describe('useQuery', () => { initialData() { return filter === '' ? initialTodos : undefined }, - keepPreviousData: true, + placeholderData: keepPreviousData, staleTime: 5000, }) diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index c59f4b341c..6e39248a02 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -1,6 +1,10 @@ import * as React from 'react' -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' @@ -25,7 +29,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/__tests__/createInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx index 1546c92db8..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 } from '..' +import { + createInfiniteQuery, + QueryCache, + QueryClientProvider, + keepPreviousData, +} from '..' import { Blink, queryKey, setActTimeout } from './utils' interface Result { @@ -95,7 +100,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -128,7 +132,6 @@ describe('useInfiniteQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -182,7 +185,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 +200,7 @@ describe('useInfiniteQuery', () => { }, getNextPageParam: () => 1, - keepPreviousData: true, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) @@ -236,28 +239,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 +268,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..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,284 +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()], - keepPreviousData: true, - queryFn: async () => { - await sleep(10) - return count() * 2 - }, - }, - { - queryKey: [key2, count()], - keepPreviousData: true, - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: 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], - keepPreviousData: true, - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 12, isPreviousData: false, isFetching: false }, - { status: 'success', data: 18, isPreviousData: 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 - }, - keepPreviousData: true, - } - }), - })) - - 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, isPreviousData: false, isFetching: false }, - { status: 'success', data: 15, isPreviousData: 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 - }, - keepPreviousData: true, - } - }), - })) - - 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, - isPreviousData: false, - isFetching: true, - }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[1]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { - status: 'loading', - data: undefined, - isPreviousData: false, - isFetching: true, - }, - ]) - expect(states[2]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[3]).toMatchObject([ - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[4]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: true }, - { status: 'success', data: 10, isPreviousData: false, isFetching: false }, - ]) - expect(states[5]).toMatchObject([ - { status: 'success', data: 5, isPreviousData: false, isFetching: false }, - { status: 'success', data: 10, isPreviousData: 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__/createQuery.test.tsx b/packages/solid-query/src/__tests__/createQuery.test.tsx index 424c7a1516..49eba723bf 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 } from '..' +import { + createQuery, + QueryCache, + QueryClientProvider, + keepPreviousData, +} from '..' import { Blink, createQueryClient, @@ -289,7 +294,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -316,7 +320,6 @@ describe('createQuery', () => { isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -378,7 +381,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -405,7 +407,6 @@ describe('createQuery', () => { isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -432,7 +433,6 @@ describe('createQuery', () => { isInitialLoading: false, isLoadingError: true, isPlaceholderData: false, - isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, @@ -1597,7 +1597,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 +1610,7 @@ describe('createQuery', () => { await sleep(10) return count() }, - keepPreviousData: true, + placeholderData: keepPreviousData, })) createRenderEffect(() => { @@ -1639,32 +1639,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 +1678,7 @@ describe('createQuery', () => { return count() }, initialData: 99, - keepPreviousData: true, + placeholderData: keepPreviousData, })) createRenderEffect(() => { @@ -1719,32 +1719,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 +1758,7 @@ describe('createQuery', () => { return count() }, enabled: false, - keepPreviousData: true, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) @@ -1797,46 +1797,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 +1854,7 @@ describe('createQuery', () => { return count() }, enabled: false, - keepPreviousData: true, + placeholderData: keepPreviousData, notifyOnChangeProps: 'all', })) @@ -1893,28 +1893,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, }) }) 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..de86b08be9 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,28 @@ 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 : // Fallback - UseQueryOptions + UseQueryOptionsForUseQueries type GetResults = // Part 1: responsible for mapping explicit type parameter to function result, if object @@ -84,7 +102,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 +113,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 +138,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,