From b6571077a804e62ad2db1d73ddca428903d283e8 Mon Sep 17 00:00:00 2001 From: Aryan Deora Date: Sat, 13 May 2023 05:20:26 -0400 Subject: [PATCH] feat(query-core): Add previousQuery to placeholderFn (#5358) * feat(query-core): Add previousQuery to placeholderFn * Add query-core and react-query tests * Remove unused query type --------- Co-authored-by: Dominik Dorfmeister --- packages/query-core/src/queryObserver.ts | 13 ++-- .../src/tests/queryObserver.test.tsx | 66 +++++++++++++++++++ packages/query-core/src/types.ts | 8 ++- .../src/__tests__/useQuery.test.tsx | 54 +++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 7f71b70624..e33db6c72b 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -65,9 +65,9 @@ export class QueryObserver< #selectError: TError | null #selectFn?: (data: TQueryData) => TData #selectResult?: TData - // This property keeps track of the last defined query data. - // It will be used to pass the previous data to the placeholder function between renders. - #lastDefinedQueryData?: TQueryData + // This property keeps track of the last query with defined data. + // It will be used to pass the previous data and query to the placeholder function between renders. + #lastQueryWithDefinedData?: Query #staleTimeoutId?: ReturnType #refetchIntervalId?: ReturnType #currentRefetchInterval?: number | false @@ -489,7 +489,10 @@ export class QueryObserver< typeof options.placeholderData === 'function' ? ( options.placeholderData as unknown as PlaceholderDataFunction - )(this.#lastDefinedQueryData) + )( + this.#lastQueryWithDefinedData?.state.data, + this.#lastQueryWithDefinedData as any, + ) : options.placeholderData if (options.select && typeof placeholderData !== 'undefined') { try { @@ -572,7 +575,7 @@ export class QueryObserver< } if (this.#currentResultState.data !== undefined) { - this.#lastDefinedQueryData = this.#currentResultState.data + this.#lastQueryWithDefinedData = this.#currentQuery } this.#currentResult = nextResult diff --git a/packages/query-core/src/tests/queryObserver.test.tsx b/packages/query-core/src/tests/queryObserver.test.tsx index 1311966132..020bb4e2d3 100644 --- a/packages/query-core/src/tests/queryObserver.test.tsx +++ b/packages/query-core/src/tests/queryObserver.test.tsx @@ -691,6 +691,72 @@ describe('queryObserver', () => { expect(observer.getCurrentResult().isPlaceholderData).toBe(false) }) + test('should pass the correct previous queryKey (from prevQuery) to placeholderData function params with select', async () => { + const results: QueryObserverResult[] = [] + const keys: Array = [] + + const key1 = queryKey() + const key2 = queryKey() + + const data1 = { value: 'data1' } + const data2 = { value: 'data2' } + + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: () => data1, + placeholderData: (prev, prevQuery) => { + keys.push(prevQuery?.queryKey || null) + return prev + }, + select: (data) => data.value, + }) + + const unsubscribe = observer.subscribe((result) => { + results.push(result) + }) + + await sleep(1) + + observer.setOptions({ + queryKey: key2, + queryFn: () => data2, + placeholderData: (prev, prevQuery) => { + keys.push(prevQuery?.queryKey || null) + return prev + }, + select: (data) => data.value, + }) + + await sleep(1) + unsubscribe() + expect(results.length).toBe(4) + expect(keys.length).toBe(3) + expect(keys[0]).toBe(null) // First Query - status: 'pending', fetchStatus: 'idle' + expect(keys[1]).toBe(null) // First Query - status: 'pending', fetchStatus: 'fetching' + expect(keys[2]).toBe(key1) // Second Query - status: 'pending', fetchStatus: 'fetching' + + expect(results[0]).toMatchObject({ + data: undefined, + status: 'pending', + fetchStatus: 'fetching', + }) // Initial fetch + expect(results[1]).toMatchObject({ + data: 'data1', + status: 'success', + fetchStatus: 'idle', + }) // Successful fetch + expect(results[2]).toMatchObject({ + data: 'data1', + status: 'success', + fetchStatus: 'fetching', + }) // Fetch for new key, but using previous data as placeholder + expect(results[3]).toMatchObject({ + data: 'data2', + status: 'success', + fetchStatus: 'idle', + }) // Successful fetch for new key + }) + test('should pass the correct previous data to placeholderData function params when select function is used in conjunction', async () => { const results: QueryObserverResult[] = [] diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 5a7361296b..b08c4d905e 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -49,8 +49,14 @@ export type InitialDataFunction = () => T | undefined type NonFunctionGuard = T extends Function ? never : T -export type PlaceholderDataFunction = ( +export type PlaceholderDataFunction< + TQueryFnData = unknown, + TError = DefaultError, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = ( previousData: TQueryData | undefined, + previousQuery: Query | undefined, ) => TQueryData | undefined export type QueriesPlaceholderDataFunction = () => diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 045c128419..e70b32e6ff 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -1542,6 +1542,60 @@ describe('useQuery', () => { }) }) + it('should keep the previous queryKey (from prevQuery) between multiple pending queries when placeholderData is set and select fn transform is used', async () => { + const keys: Array = [] + const key = queryKey() + const states: UseQueryResult[] = [] + + function Page() { + const [count, setCount] = React.useState(0) + + const state = useQuery({ + queryKey: [key, count], + queryFn: async () => { + await sleep(10) + return { + count, + } + }, + select(data) { + return data.count + }, + placeholderData: (prevData, prevQuery) => { + if (prevQuery) { + keys.push(prevQuery.queryKey) + } + return prevData + }, + }) + + states.push(state) + + return ( +
+
data: {state.data}
+ +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 0')) + + fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) + fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) + fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) + + await waitFor(() => rendered.getByText('data: 3')) + + const allPreviousKeysAreTheFirstQueryKey = keys.every( + (k) => JSON.stringify(k) === JSON.stringify([key, 0]), + ) + + expect(allPreviousKeysAreTheFirstQueryKey).toBe(true) + }) + it('should show placeholderData between multiple pending queries when select fn transform is used', async () => { const key = queryKey() const states: UseQueryResult[] = []