Skip to content

Commit 75a2b4d

Browse files
authored
fix(query-core): correct placeholderData prevData value with select fn (#5227)
* fix(query-core): correct placeholderData prevData * fix(query-core): preserves correct prevQueryResult This commit preserves the correct previous result between rerenders. * test(react-query): Test with placeholder & select * fix(query-core): Add lastDefinedQueryData property * fix(query-core): Remove console.log * fix(query-core): Add react-query test
1 parent 67a563c commit 75a2b4d

File tree

3 files changed

+220
-7
lines changed

3 files changed

+220
-7
lines changed

packages/query-core/src/queryObserver.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ export class QueryObserver<
6464
TQueryData,
6565
TQueryKey
6666
>
67-
#previousQueryResult?: QueryObserverResult<TData, TError>
6867
#selectError: TError | null
6968
#selectFn?: (data: TQueryData) => TData
7069
#selectResult?: TData
70+
// This property keeps track of the last defined query data.
71+
// It will be used to pass the previous data to the placeholder function between renders.
72+
#lastDefinedQueryData?: TQueryData
7173
#staleTimeoutId?: ReturnType<typeof setTimeout>
7274
#refetchIntervalId?: ReturnType<typeof setInterval>
7375
#currentRefetchInterval?: number | false
@@ -414,9 +416,6 @@ export class QueryObserver<
414416
const queryInitialState = queryChange
415417
? query.state
416418
: this.#currentQueryInitialState
417-
const prevQueryResult = queryChange
418-
? this.#currentResult
419-
: this.#previousQueryResult
420419

421420
const { state } = query
422421
let { error, errorUpdatedAt, fetchStatus, status } = state
@@ -490,7 +489,7 @@ export class QueryObserver<
490489
typeof options.placeholderData === 'function'
491490
? (
492491
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
493-
)(prevQueryResult?.data as TQueryData | undefined)
492+
)(this.#lastDefinedQueryData)
494493
: options.placeholderData
495494
if (options.select && typeof placeholderData !== 'undefined') {
496495
try {
@@ -504,7 +503,11 @@ export class QueryObserver<
504503

505504
if (typeof placeholderData !== 'undefined') {
506505
status = 'success'
507-
data = replaceData(prevResult?.data, placeholderData, options) as TData
506+
data = replaceData(
507+
prevResult?.data,
508+
placeholderData as unknown,
509+
options,
510+
) as TData
508511
isPlaceholderData = true
509512
}
510513
}
@@ -568,6 +571,9 @@ export class QueryObserver<
568571
return
569572
}
570573

574+
if (this.#currentResultState.data !== undefined) {
575+
this.#lastDefinedQueryData = this.#currentResultState.data
576+
}
571577
this.#currentResult = nextResult
572578

573579
// Determine which callbacks to trigger
@@ -619,7 +625,6 @@ export class QueryObserver<
619625
| undefined
620626
this.#currentQuery = query
621627
this.#currentQueryInitialState = query.state
622-
this.#previousQueryResult = this.#currentResult
623628

624629
if (this.hasListeners()) {
625630
prevQuery?.removeObserver(this)

packages/query-core/src/tests/queryObserver.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,61 @@ describe('queryObserver', () => {
691691
expect(observer.getCurrentResult().isPlaceholderData).toBe(false)
692692
})
693693

694+
test('should pass the correct previous data to placeholderData function params when select function is used in conjunction', async () => {
695+
const results: QueryObserverResult[] = []
696+
697+
const key1 = queryKey()
698+
const key2 = queryKey()
699+
700+
const data1 = { value: 'data1' }
701+
const data2 = { value: 'data2' }
702+
703+
const observer = new QueryObserver(queryClient, {
704+
queryKey: key1,
705+
queryFn: () => data1,
706+
placeholderData: (prev) => prev,
707+
select: (data) => data.value,
708+
})
709+
710+
const unsubscribe = observer.subscribe((result) => {
711+
results.push(result)
712+
})
713+
714+
await sleep(1)
715+
716+
observer.setOptions({
717+
queryKey: key2,
718+
queryFn: () => data2,
719+
placeholderData: (prev) => prev,
720+
select: (data) => data.value,
721+
})
722+
723+
await sleep(1)
724+
unsubscribe()
725+
726+
expect(results.length).toBe(4)
727+
expect(results[0]).toMatchObject({
728+
data: undefined,
729+
status: 'pending',
730+
fetchStatus: 'fetching',
731+
}) // Initial fetch
732+
expect(results[1]).toMatchObject({
733+
data: 'data1',
734+
status: 'success',
735+
fetchStatus: 'idle',
736+
}) // Successful fetch
737+
expect(results[2]).toMatchObject({
738+
data: 'data1',
739+
status: 'success',
740+
fetchStatus: 'fetching',
741+
}) // Fetch for new key, but using previous data as placeholder
742+
expect(results[3]).toMatchObject({
743+
data: 'data2',
744+
status: 'success',
745+
fetchStatus: 'idle',
746+
}) // Successful fetch for new key
747+
})
748+
694749
test('setOptions should notify cache listeners', async () => {
695750
const key = queryKey()
696751

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,159 @@ describe('useQuery', () => {
17281728
})
17291729
})
17301730

1731+
it('should keep the previous data when placeholderData is set and select fn transform is used', async () => {
1732+
const key = queryKey()
1733+
const states: UseQueryResult<number>[] = []
1734+
1735+
function Page() {
1736+
const [count, setCount] = React.useState(0)
1737+
1738+
const state = useQuery({
1739+
queryKey: [key, count],
1740+
queryFn: async () => {
1741+
await sleep(10)
1742+
return {
1743+
count,
1744+
}
1745+
},
1746+
select(data) {
1747+
return data.count
1748+
},
1749+
placeholderData: keepPreviousData,
1750+
})
1751+
1752+
states.push(state)
1753+
1754+
return (
1755+
<div>
1756+
<div>data: {state.data}</div>
1757+
<button onClick={() => setCount(1)}>setCount</button>
1758+
</div>
1759+
)
1760+
}
1761+
1762+
const rendered = renderWithClient(queryClient, <Page />)
1763+
1764+
await waitFor(() => rendered.getByText('data: 0'))
1765+
1766+
fireEvent.click(rendered.getByRole('button', { name: 'setCount' }))
1767+
1768+
await waitFor(() => rendered.getByText('data: 1'))
1769+
1770+
// Initial
1771+
expect(states[0]).toMatchObject({
1772+
data: undefined,
1773+
isFetching: true,
1774+
isSuccess: false,
1775+
isPlaceholderData: false,
1776+
})
1777+
// Fetched
1778+
expect(states[1]).toMatchObject({
1779+
data: 0,
1780+
isFetching: false,
1781+
isSuccess: true,
1782+
isPlaceholderData: false,
1783+
})
1784+
// Set state
1785+
expect(states[2]).toMatchObject({
1786+
data: 0,
1787+
isFetching: true,
1788+
isSuccess: true,
1789+
isPlaceholderData: true,
1790+
})
1791+
// New data
1792+
expect(states[3]).toMatchObject({
1793+
data: 1,
1794+
isFetching: false,
1795+
isSuccess: true,
1796+
isPlaceholderData: false,
1797+
})
1798+
})
1799+
1800+
it('should show placeholderData between multiple pending queries when select fn transform is used', async () => {
1801+
const key = queryKey()
1802+
const states: UseQueryResult<number>[] = []
1803+
1804+
function Page() {
1805+
const [count, setCount] = React.useState(0)
1806+
1807+
const state = useQuery({
1808+
queryKey: [key, count],
1809+
queryFn: async () => {
1810+
await sleep(10)
1811+
return {
1812+
count,
1813+
}
1814+
},
1815+
select(data) {
1816+
return data.count
1817+
},
1818+
placeholderData: keepPreviousData,
1819+
})
1820+
1821+
states.push(state)
1822+
1823+
return (
1824+
<div>
1825+
<div>data: {state.data}</div>
1826+
<button onClick={() => setCount((prev) => prev + 1)}>setCount</button>
1827+
</div>
1828+
)
1829+
}
1830+
1831+
const rendered = renderWithClient(queryClient, <Page />)
1832+
1833+
await waitFor(() => rendered.getByText('data: 0'))
1834+
1835+
fireEvent.click(rendered.getByRole('button', { name: 'setCount' }))
1836+
fireEvent.click(rendered.getByRole('button', { name: 'setCount' }))
1837+
fireEvent.click(rendered.getByRole('button', { name: 'setCount' }))
1838+
1839+
await waitFor(() => rendered.getByText('data: 3'))
1840+
// Initial
1841+
expect(states[0]).toMatchObject({
1842+
data: undefined,
1843+
isFetching: true,
1844+
isSuccess: false,
1845+
isPlaceholderData: false,
1846+
})
1847+
// Fetched
1848+
expect(states[1]).toMatchObject({
1849+
data: 0,
1850+
isFetching: false,
1851+
isSuccess: true,
1852+
isPlaceholderData: false,
1853+
})
1854+
// Set state -> count = 1
1855+
expect(states[2]).toMatchObject({
1856+
data: 0,
1857+
isFetching: true,
1858+
isSuccess: true,
1859+
isPlaceholderData: true,
1860+
})
1861+
// Set state -> count = 2
1862+
expect(states[3]).toMatchObject({
1863+
data: 0,
1864+
isFetching: true,
1865+
isSuccess: true,
1866+
isPlaceholderData: true,
1867+
})
1868+
// Set state -> count = 3
1869+
expect(states[4]).toMatchObject({
1870+
data: 0,
1871+
isFetching: true,
1872+
isSuccess: true,
1873+
isPlaceholderData: true,
1874+
})
1875+
// New data
1876+
expect(states[5]).toMatchObject({
1877+
data: 3,
1878+
isFetching: false,
1879+
isSuccess: true,
1880+
isPlaceholderData: false,
1881+
})
1882+
})
1883+
17311884
it('should transition to error state when placeholderData is set', async () => {
17321885
const key = queryKey()
17331886
const states: UseQueryResult<number>[] = []

0 commit comments

Comments
 (0)