Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove keepPreviousData in favor of placeholderData #4715

Merged
merged 14 commits into from
Jan 13, 2023
Merged
54 changes: 48 additions & 6 deletions docs/react/guides/migrating-to-v5.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
id: migrating-to-v5
id: migrating-to-react-query-5
title: Migrating to TanStack Query v5
---

Expand Down Expand Up @@ -150,18 +150,60 @@ If you want to throw something that isn't an Error, you'll now have to set the g
useQuery<number, string>({
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
},
})
```

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

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).
Expand Down
20 changes: 10 additions & 10 deletions docs/react/guides/paginated-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,11 +41,11 @@ function Todos() {
error,
data,
isFetching,
isPreviousData,
isPlaceholderData,
} = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
keepPreviousData : true
placeholderData: keepPreviousData,
})

return (
Expand All @@ -70,12 +70,12 @@ function Todos() {
</button>{' '}
<button
onClick={() => {
if (!isPreviousData && data.hasMore) {
if (!isPlaceholderData && data.hasMore) {
setPage(old => old + 1)
}
}}
// Disable the Next Page button until we know a next page is available
disabled={isPreviousData || !data?.hasMore}
disabled={isPlaceholderData || !data?.hasMore}
>
Next Page
</button>
Expand All @@ -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.
2 changes: 1 addition & 1 deletion docs/react/reference/QueryClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
13 changes: 3 additions & 10 deletions docs/react/reference/useQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const {
isLoading,
isLoadingError,
isPlaceholderData,
isPreviousData,
isRefetchError,
isRefetching,
isStale,
Expand All @@ -35,7 +34,6 @@ const {
networkMode,
initialData,
initialDataUpdatedAt,
keepPreviousData,
meta,
notifyOnChangeProps,
onError,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`
Expand Down
11 changes: 6 additions & 5 deletions examples/react/pagination/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useQueryClient,
QueryClient,
QueryClientProvider,
keepPreviousData,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

Expand All @@ -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 (
<div>
Expand Down Expand Up @@ -78,7 +79,7 @@ function Example() {
onClick={() => {
setPage((old) => (data?.hasMore ? old + 1 : old))
}}
disabled={isPreviousData || !data?.hasMore}
disabled={isPlaceholderData || !data?.hasMore}
>
Next Page
</button>
Expand Down
8 changes: 7 additions & 1 deletion packages/query-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 1 addition & 16 deletions packages/query-core/src/queriesObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,29 +155,14 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
!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!]
return currentObserver ?? new QueryObserver(this.client, defaultedOptions)
}

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),
Expand Down
26 changes: 7 additions & 19 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -440,7 +439,7 @@ export class QueryObserver<
fetchStatus = canFetch(query.options.networkMode)
? 'fetching'
: 'paused'
if (!dataUpdatedAt) {
if (!state.dataUpdatedAt) {
status = 'loading'
}
}
Expand All @@ -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 &&
Expand Down Expand Up @@ -507,7 +494,9 @@ export class QueryObserver<
} else {
placeholderData =
typeof options.placeholderData === 'function'
? (options.placeholderData as PlaceholderDataFunction<TQueryData>)()
? (
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
)(prevQueryResult?.data as TQueryData | undefined)
: options.placeholderData
if (options.select && typeof placeholderData !== 'undefined') {
try {
Expand Down Expand Up @@ -548,7 +537,7 @@ export class QueryObserver<
isError,
isInitialLoading: isLoading && isFetching,
data,
dataUpdatedAt,
dataUpdatedAt: state.dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: state.fetchFailureCount,
Expand All @@ -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,
Expand Down
20 changes: 12 additions & 8 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ export interface QueryFunctionContext<

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

export type PlaceholderDataFunction<TResult> = () => TResult | undefined
type NonFunctionGuard<T> = T extends Function ? never : T

export type PlaceholderDataFunction<TQueryData> = (
previousData: TQueryData | undefined,
) => TQueryData | undefined

export type QueriesPlaceholderDataFunction<TQueryData> = () =>
| TQueryData
| undefined

export type QueryKeyHashFunction<TQueryKey extends QueryKey> = (
queryKey: TQueryKey,
Expand Down Expand Up @@ -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<TQueryData>
placeholderData?:
| NonFunctionGuard<TQueryData>
| PlaceholderDataFunction<NonFunctionGuard<TQueryData>>

_optimisticResults?: 'optimistic' | 'isRestoring'
}
Expand Down Expand Up @@ -379,7 +384,6 @@ export interface QueryObserverBaseResult<TData = unknown, TError = Error> {
isInitialLoading: boolean
isPaused: boolean
isPlaceholderData: boolean
isPreviousData: boolean
isRefetchError: boolean
isRefetching: boolean
isStale: boolean
Expand Down
6 changes: 6 additions & 0 deletions packages/query-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,9 @@ export function replaceData<
}
return data
}

export function keepPreviousData<T>(
previousData: T | undefined,
): T | undefined {
return previousData
}
Loading