Skip to content

Commit

Permalink
feat(query-core): Add previousQuery to placeholderFn (#5358)
Browse files Browse the repository at this point in the history
* feat(query-core): Add previousQuery to placeholderFn

* Add query-core and react-query tests

* Remove unused query type

---------

Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc>
  • Loading branch information
ardeora and TkDodo authored May 13, 2023
1 parent 0b7235e commit b657107
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 6 deletions.
13 changes: 8 additions & 5 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TQueryFnData, TError, TQueryData, TQueryKey>
#staleTimeoutId?: ReturnType<typeof setTimeout>
#refetchIntervalId?: ReturnType<typeof setInterval>
#currentRefetchInterval?: number | false
Expand Down Expand Up @@ -489,7 +489,10 @@ export class QueryObserver<
typeof options.placeholderData === 'function'
? (
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
)(this.#lastDefinedQueryData)
)(
this.#lastQueryWithDefinedData?.state.data,
this.#lastQueryWithDefinedData as any,
)
: options.placeholderData
if (options.select && typeof placeholderData !== 'undefined') {
try {
Expand Down Expand Up @@ -572,7 +575,7 @@ export class QueryObserver<
}

if (this.#currentResultState.data !== undefined) {
this.#lastDefinedQueryData = this.#currentResultState.data
this.#lastQueryWithDefinedData = this.#currentQuery
}
this.#currentResult = nextResult

Expand Down
66 changes: 66 additions & 0 deletions packages/query-core/src/tests/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly unknown[] | null> = []

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[] = []

Expand Down
8 changes: 7 additions & 1 deletion packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ export type InitialDataFunction<T> = () => T | undefined

type NonFunctionGuard<T> = T extends Function ? never : T

export type PlaceholderDataFunction<TQueryData> = (
export type PlaceholderDataFunction<
TQueryFnData = unknown,
TError = DefaultError,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = (
previousData: TQueryData | undefined,
previousQuery: Query<TQueryFnData, TError, TQueryData, TQueryKey> | undefined,
) => TQueryData | undefined

export type QueriesPlaceholderDataFunction<TQueryData> = () =>
Expand Down
54 changes: 54 additions & 0 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<readonly unknown[] | null> = []
const key = queryKey()
const states: UseQueryResult<number>[] = []

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 (
<div>
<div>data: {state.data}</div>
<button onClick={() => setCount((prev) => prev + 1)}>setCount</button>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)

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<number>[] = []
Expand Down

0 comments on commit b657107

Please sign in to comment.