Skip to content

Commit c01c3ff

Browse files
feat: remove keepPreviousData in favor of placeholderData (#4715)
* feat: remove `keepPreviousData` in favor of `placeholderData` BREAKING CHANGE: removed `keepPreviousData` in favor of `placeholderData` identity function * fix: change generic name * docs: extend diff in migration guide * tests: remove irrelevant tests * fix: useQueries placeholder data parameter removed * feat: keepPreviousData identity function * fix: formatting * fix: ts 4.7 error * fix: formatting * docs: add a note about useQueries * chore: fix formatting * fix: formatting * Apply suggestions from code review Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc>
1 parent 096da93 commit c01c3ff

File tree

19 files changed

+253
-722
lines changed

19 files changed

+253
-722
lines changed

docs/react/guides/migrating-to-v5.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
id: migrating-to-v5
2+
id: migrating-to-react-query-5
33
title: Migrating to TanStack Query v5
44
---
55

@@ -150,18 +150,60 @@ If you want to throw something that isn't an Error, you'll now have to set the g
150150
useQuery<number, string>({
151151
queryKey: ['some-query'],
152152
queryFn: async () => {
153-
if (Math.random() > 0.5) {
154-
throw 'some error'
155-
}
156-
return 42
157-
}
153+
if (Math.random() > 0.5) {
154+
throw 'some error'
155+
}
156+
return 42
157+
},
158158
})
159159
```
160160

161161
### eslint `prefer-query-object-syntax` rule is removed
162162

163163
Since the only supported syntax now is the object syntax, this rule is no longer needed
164164

165+
### Removed `keepPreviousData` in favor of `placeholderData` identity function
166+
167+
We have removed the `keepPreviousData` option and `isPreviousData` flag as they were doing mostly the same thing as `placeholderData` and `isPlaceholderData` flag.
168+
169+
To achieve the same functionality as `keepPreviousData`, we have added previous query `data` as an argument to `placeholderData` function.
170+
Therefore you just need to provide an identity function to `placeholderData` or use `keepPreviousData` function returned from Tanstack Query.
171+
172+
> 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.
173+
174+
```diff
175+
const {
176+
data,
177+
- isPreviousData,
178+
+ isPlaceholderData,
179+
} = useQuery({
180+
queryKey,
181+
queryFn,
182+
- keepPreviousData: true,
183+
+ placeholderData: keepPreviousData
184+
});
185+
```
186+
187+
There are some caveats to this change however, which you must be aware of:
188+
189+
- `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`.
190+
- `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`.
191+
192+
```ts
193+
const [updatedAt, setUpdatedAt] = useState(0)
194+
195+
const { data, dataUpdatedAt } = useQuery({
196+
queryKey: ['projects', page],
197+
queryFn: () => fetchProjects(page),
198+
})
199+
200+
useEffect(() => {
201+
if (dataUpdatedAt > updatedAt) {
202+
setUpdatedAt(dataUpdatedAt)
203+
}
204+
}, [dataUpdatedAt])
205+
```
206+
165207
### Window focus refetching no longer listens to the `focus` event
166208

167209
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).

docs/react/guides/paginated-queries.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ However, if you run this simple example, you might notice something strange:
1818

1919
**The UI jumps in and out of the `success` and `loading` states because each new page is treated like a brand new query.**
2020

21-
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.
21+
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.
2222

23-
## Better Paginated Queries with `keepPreviousData`
23+
## Better Paginated Queries with `placeholderData`
2424

25-
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:
25+
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:
2626

2727
- **The data from the last successful fetch available while new data is being requested, even though the query key has changed**.
2828
- When the new data arrives, the previous `data` is seamlessly swapped to show the new data.
29-
- `isPreviousData` is made available to know what data the query is currently providing you
29+
- `isPlaceholderData` is made available to know what data the query is currently providing you
3030

3131
[//]: # 'Example2'
3232
```tsx
@@ -41,11 +41,11 @@ function Todos() {
4141
error,
4242
data,
4343
isFetching,
44-
isPreviousData,
44+
isPlaceholderData,
4545
} = useQuery({
4646
queryKey: ['projects', page],
4747
queryFn: () => fetchProjects(page),
48-
keepPreviousData : true
48+
placeholderData: keepPreviousData,
4949
})
5050

5151
return (
@@ -70,12 +70,12 @@ function Todos() {
7070
</button>{' '}
7171
<button
7272
onClick={() => {
73-
if (!isPreviousData && data.hasMore) {
73+
if (!isPlaceholderData && data.hasMore) {
7474
setPage(old => old + 1)
7575
}
7676
}}
7777
// Disable the Next Page button until we know a next page is available
78-
disabled={isPreviousData || !data?.hasMore}
78+
disabled={isPlaceholderData || !data?.hasMore}
7979
>
8080
Next Page
8181
</button>
@@ -86,6 +86,6 @@ function Todos() {
8686
```
8787
[//]: # 'Example2'
8888

89-
## Lagging Infinite Query results with `keepPreviousData`
89+
## Lagging Infinite Query results with `placeholderData`
9090

91-
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.
91+
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.

docs/react/reference/QueryClient.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ try {
9595

9696
**Options**
9797

98-
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.
98+
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.
9999

100100
**Returns**
101101

docs/react/reference/useQuery.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const {
1919
isLoading,
2020
isLoadingError,
2121
isPlaceholderData,
22-
isPreviousData,
2322
isRefetchError,
2423
isRefetching,
2524
isStale,
@@ -35,7 +34,6 @@ const {
3534
networkMode,
3635
initialData,
3736
initialDataUpdatedAt,
38-
keepPreviousData,
3937
meta,
4038
notifyOnChangeProps,
4139
onError,
@@ -160,15 +158,12 @@ const {
160158
- `initialDataUpdatedAt: number | (() => number | undefined)`
161159
- Optional
162160
- If set, this value will be used as the time (in milliseconds) of when the `initialData` itself was last updated.
163-
- `placeholderData: TData | () => TData`
161+
- `placeholderData: TData | (previousValue: TData) => TData`
164162
- Optional
165163
- 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.
166164
- `placeholderData` is **not persisted** to the cache
167-
- `keepPreviousData: boolean`
168-
- Optional
169-
- Defaults to `false`
170-
- If set, any previous `data` will be kept when fetching new data because the query key changed.
171-
`structuralSharing: boolean | ((oldData: TData | undefined, newData: TData) => TData)`
165+
- If you provide a function for `placeholderData`, as a first argument you will receive previously watched query data if available
166+
- `structuralSharing: boolean | ((oldData: TData | undefined, newData: TData) => TData)`
172167
- Optional
173168
- Defaults to `true`
174169
- If set to `false`, structural sharing between query results will be disabled.
@@ -215,8 +210,6 @@ const {
215210
- Will be `true` if the data in the cache is invalidated or if the data is older than the given `staleTime`.
216211
- `isPlaceholderData: boolean`
217212
- Will be `true` if the data shown is the placeholder data.
218-
- `isPreviousData: boolean`
219-
- Will be `true` when `keepPreviousData` is set and data from the previous query is returned.
220213
- `isFetched: boolean`
221214
- Will be `true` if the query has been fetched.
222215
- `isFetchedAfterMount: boolean`

examples/react/pagination/pages/index.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useQueryClient,
66
QueryClient,
77
QueryClientProvider,
8+
keepPreviousData,
89
} from '@tanstack/react-query'
910
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
1011

@@ -27,22 +28,22 @@ function Example() {
2728
const queryClient = useQueryClient()
2829
const [page, setPage] = React.useState(0)
2930

30-
const { status, data, error, isFetching, isPreviousData } = useQuery({
31+
const { status, data, error, isFetching, isPlaceholderData } = useQuery({
3132
queryKey: ['projects', page],
3233
queryFn: () => fetchProjects(page),
33-
keepPreviousData: true,
34+
placeholderData: keepPreviousData,
3435
staleTime: 5000,
3536
})
3637

3738
// Prefetch the next page!
3839
React.useEffect(() => {
39-
if (!isPreviousData && data?.hasMore) {
40+
if (!isPlaceholderData && data?.hasMore) {
4041
queryClient.prefetchQuery({
4142
queryKey: ['projects', page + 1],
4243
queryFn: () => fetchProjects(page + 1),
4344
})
4445
}
45-
}, [data, isPreviousData, page, queryClient])
46+
}, [data, isPlaceholderData, page, queryClient])
4647

4748
return (
4849
<div>
@@ -78,7 +79,7 @@ function Example() {
7879
onClick={() => {
7980
setPage((old) => (data?.hasMore ? old + 1 : old))
8081
}}
81-
disabled={isPreviousData || !data?.hasMore}
82+
disabled={isPlaceholderData || !data?.hasMore}
8283
>
8384
Next Page
8485
</button>

packages/query-core/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ export { MutationObserver } from './mutationObserver'
1111
export { notifyManager } from './notifyManager'
1212
export { focusManager } from './focusManager'
1313
export { onlineManager } from './onlineManager'
14-
export { hashQueryKey, replaceEqualDeep, isError, isServer } from './utils'
14+
export {
15+
hashQueryKey,
16+
replaceEqualDeep,
17+
isError,
18+
isServer,
19+
keepPreviousData,
20+
} from './utils'
1521
export type { MutationFilters, QueryFilters, Updater } from './utils'
1622
export { isCancelledError } from './retryer'
1723
export { dehydrate, hydrate } from './hydration'

packages/query-core/src/queriesObserver.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,29 +155,14 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
155155
!matchedQueryHashes.includes(defaultedOptions.queryHash),
156156
)
157157

158-
const unmatchedObservers = prevObservers.filter(
159-
(prevObserver) =>
160-
!matchingObservers.some((match) => match.observer === prevObserver),
161-
)
162-
163158
const getObserver = (options: QueryObserverOptions): QueryObserver => {
164159
const defaultedOptions = this.client.defaultQueryOptions(options)
165160
const currentObserver = this.observersMap[defaultedOptions.queryHash!]
166161
return currentObserver ?? new QueryObserver(this.client, defaultedOptions)
167162
}
168163

169164
const newOrReusedObservers: QueryObserverMatch[] = unmatchedQueries.map(
170-
(options, index) => {
171-
if (options.keepPreviousData) {
172-
// return previous data from one of the observers that no longer match
173-
const previouslyUsedObserver = unmatchedObservers[index]
174-
if (previouslyUsedObserver !== undefined) {
175-
return {
176-
defaultedQueryOptions: options,
177-
observer: previouslyUsedObserver,
178-
}
179-
}
180-
}
165+
(options) => {
181166
return {
182167
defaultedQueryOptions: options,
183168
observer: getObserver(options),

packages/query-core/src/queryObserver.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,7 @@ export class QueryObserver<
422422
: this.previousQueryResult
423423

424424
const { state } = query
425-
let { dataUpdatedAt, error, errorUpdatedAt, fetchStatus, status } = state
426-
let isPreviousData = false
425+
let { error, errorUpdatedAt, fetchStatus, status } = state
427426
let isPlaceholderData = false
428427
let data: TData | undefined
429428

@@ -440,7 +439,7 @@ export class QueryObserver<
440439
fetchStatus = canFetch(query.options.networkMode)
441440
? 'fetching'
442441
: 'paused'
443-
if (!dataUpdatedAt) {
442+
if (!state.dataUpdatedAt) {
444443
status = 'loading'
445444
}
446445
}
@@ -449,20 +448,8 @@ export class QueryObserver<
449448
}
450449
}
451450

452-
// Keep previous data if needed
453-
if (
454-
options.keepPreviousData &&
455-
!state.dataUpdatedAt &&
456-
prevQueryResult?.isSuccess &&
457-
status !== 'error'
458-
) {
459-
data = prevQueryResult.data
460-
dataUpdatedAt = prevQueryResult.dataUpdatedAt
461-
status = prevQueryResult.status
462-
isPreviousData = true
463-
}
464451
// Select data if needed
465-
else if (options.select && typeof state.data !== 'undefined') {
452+
if (options.select && typeof state.data !== 'undefined') {
466453
// Memoize select result
467454
if (
468455
prevResult &&
@@ -507,7 +494,9 @@ export class QueryObserver<
507494
} else {
508495
placeholderData =
509496
typeof options.placeholderData === 'function'
510-
? (options.placeholderData as PlaceholderDataFunction<TQueryData>)()
497+
? (
498+
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
499+
)(prevQueryResult?.data as TQueryData | undefined)
511500
: options.placeholderData
512501
if (options.select && typeof placeholderData !== 'undefined') {
513502
try {
@@ -548,7 +537,7 @@ export class QueryObserver<
548537
isError,
549538
isInitialLoading: isLoading && isFetching,
550539
data,
551-
dataUpdatedAt,
540+
dataUpdatedAt: state.dataUpdatedAt,
552541
error,
553542
errorUpdatedAt,
554543
failureCount: state.fetchFailureCount,
@@ -563,7 +552,6 @@ export class QueryObserver<
563552
isLoadingError: isError && state.dataUpdatedAt === 0,
564553
isPaused: fetchStatus === 'paused',
565554
isPlaceholderData,
566-
isPreviousData,
567555
isRefetchError: isError && state.dataUpdatedAt !== 0,
568556
isStale: isStale(query, options),
569557
refetch: this.refetch,

packages/query-core/src/types.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ export interface QueryFunctionContext<
2727

2828
export type InitialDataFunction<T> = () => T | undefined
2929

30-
export type PlaceholderDataFunction<TResult> = () => TResult | undefined
30+
type NonFunctionGuard<T> = T extends Function ? never : T
31+
32+
export type PlaceholderDataFunction<TQueryData> = (
33+
previousData: TQueryData | undefined,
34+
) => TQueryData | undefined
35+
36+
export type QueriesPlaceholderDataFunction<TQueryData> = () =>
37+
| TQueryData
38+
| undefined
3139

3240
export type QueryKeyHashFunction<TQueryKey extends QueryKey> = (
3341
queryKey: TQueryKey,
@@ -231,15 +239,12 @@ export interface QueryObserverOptions<
231239
* Defaults to `false`.
232240
*/
233241
suspense?: boolean
234-
/**
235-
* Set this to `true` to keep the previous `data` when fetching based on a new query key.
236-
* Defaults to `false`.
237-
*/
238-
keepPreviousData?: boolean
239242
/**
240243
* 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.
241244
*/
242-
placeholderData?: TQueryData | PlaceholderDataFunction<TQueryData>
245+
placeholderData?:
246+
| NonFunctionGuard<TQueryData>
247+
| PlaceholderDataFunction<NonFunctionGuard<TQueryData>>
243248

244249
_optimisticResults?: 'optimistic' | 'isRestoring'
245250
}
@@ -379,7 +384,6 @@ export interface QueryObserverBaseResult<TData = unknown, TError = Error> {
379384
isInitialLoading: boolean
380385
isPaused: boolean
381386
isPlaceholderData: boolean
382-
isPreviousData: boolean
383387
isRefetchError: boolean
384388
isRefetching: boolean
385389
isStale: boolean

packages/query-core/src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,9 @@ export function replaceData<
357357
}
358358
return data
359359
}
360+
361+
export function keepPreviousData<T>(
362+
previousData: T | undefined,
363+
): T | undefined {
364+
return previousData
365+
}

0 commit comments

Comments
 (0)