Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/react/guides/migrating-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ A few notes about how codemod works:
- If the codemod cannot infer the usage, then it will leave a message on the console. The message contains the file name and the line number of the usage. In this case, you need to do the migration manually.
- If the transformation results in an error, you will also see a message on the console. This message will notify you something unexpected happened, please do the migration manually.

### Callbacks on useQuery (and QueryObserver) have been removed

`onSuccess`, `onError` and `onSettled` have been removed from Queries. They haven't been touched for Mutations. Please see [this RFC](https://github.com/TanStack/query/discussions/5279) for motivations behind this change and what to do instead.

### The `remove` method has been removed from useQuery

Previously, remove method used to remove the query from the queryCache without informing observers about it. It was best used to remove data imperatively that is no longer needed, e.g. when logging a user out.
Expand Down Expand Up @@ -333,6 +337,10 @@ Previously, we've allowed to overwrite the `pageParams` that would be returned f

## React Query Breaking Changes

### The minimum required React version is now 18.0

React Query v5 requires React 18.0 or later. This is because we are using the new `useSyncExternalStore` hook, which is only available in React 18.0 and later. Previously, we have been using the shim provided by React.

### The `contextSharing` prop has been removed from QueryClientProvider

You could previously use the `contextSharing` property to share the first (and at least one) instance of the query client context across the window. This ensured that if TanStack Query was used across different bundles or microfrontends then they will all use the same instance of the context, regardless of module scoping.
Expand Down Expand Up @@ -387,6 +395,8 @@ To understand the reasoning behing this change checkout the [v5 roadmap discussi

## New Features 🚀

v5 also comes with new features:

### Simplified optimistic updates

We have a new, simplified way to perform optimistic updates by leveraging the returned `variables` from `useMutation`:
Expand Down
7 changes: 0 additions & 7 deletions packages/query-core/src/queriesObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,6 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
(defaultedOptions) => !matchedQueryHashes.has(defaultedOptions.queryHash),
)

const matchingObserversSet = new Set(
matchingObservers.map((match) => match.observer),
)
const unmatchedObservers = prevObservers.filter(
(prevObserver) => !matchingObserversSet.has(prevObserver),
)

const getObserver = (options: QueryObserverOptions): QueryObserver => {
const defaultedOptions = this.#client.defaultQueryOptions(options)
const currentObserver = this.#observers.find(
Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ export class Query<

notifyManager.batch(() => {
this.#observers.forEach((observer) => {
observer.onQueryUpdate(action)
observer.onQueryUpdate()
})

this.#cache.notify({ query: this, type: 'updated', action })
Expand Down
29 changes: 5 additions & 24 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,18 @@ import type {
QueryOptions,
RefetchOptions,
} from './types'
import type { Query, QueryState, Action, FetchOptions } from './query'
import type { Query, QueryState, FetchOptions } from './query'
import type { QueryClient } from './queryClient'
import { focusManager } from './focusManager'
import { Subscribable } from './subscribable'
import { canFetch, isCancelledError } from './retryer'
import { canFetch } from './retryer'

type QueryObserverListener<TData, TError> = (
result: QueryObserverResult<TData, TError>,
) => void

export interface NotifyOptions {
listeners?: boolean
onError?: boolean
onSuccess?: boolean
}

export interface ObserverFetchOptions extends FetchOptions {
Expand Down Expand Up @@ -632,16 +630,8 @@ export class QueryObserver<
}
}

onQueryUpdate(action: Action<TData, TError>): void {
const notifyOptions: NotifyOptions = {}

if (action.type === 'success') {
notifyOptions.onSuccess = !action.manual
} else if (action.type === 'error' && !isCancelledError(action.error)) {
notifyOptions.onError = true
}

this.#updateResult(notifyOptions)
onQueryUpdate(): void {
this.#updateResult()

if (this.hasListeners()) {
this.#updateTimers()
Expand All @@ -650,16 +640,7 @@ export class QueryObserver<

#notify(notifyOptions: NotifyOptions): void {
notifyManager.batch(() => {
// First trigger the configuration callbacks
if (notifyOptions.onSuccess) {
this.options.onSuccess?.(this.#currentResult.data!)
this.options.onSettled?.(this.#currentResult.data, null)
} else if (notifyOptions.onError) {
this.options.onError?.(this.#currentResult.error!)
this.options.onSettled?.(undefined, this.#currentResult.error)
}

// Then trigger the listeners
// First, trigger the listeners
if (notifyOptions.listeners) {
this.listeners.forEach((listener) => {
listener(this.#currentResult)
Expand Down
12 changes: 0 additions & 12 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,18 +259,6 @@ export interface QueryObserverOptions<
* By default, access to properties will be tracked, and the component will only re-render when one of the tracked properties change.
*/
notifyOnChangeProps?: Array<keyof InfiniteQueryObserverResult> | 'all'
/**
* This callback will fire any time the query successfully fetches new data.
*/
onSuccess?: (data: TData) => void
/**
* This callback will fire if the query encounters an error and will be passed the error.
*/
onError?: (err: TError) => void
/**
* This callback will fire any time the query is either successfully fetched or errors and be passed either the data or error.
*/
onSettled?: (data: TData | undefined, error: TError | null) => void
/**
* Whether errors should be thrown instead of setting the `error` property.
* If set to `true` or `suspense` is `true`, all errors will be thrown to the error boundary.
Expand Down
82 changes: 0 additions & 82 deletions packages/react-query/src/__tests__/suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,88 +190,6 @@ describe("useQuery's in Suspense mode", () => {
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(0)
})

it('should call onSuccess on the first successful call', async () => {
const key = queryKey()

const successFn = vi.fn()

function Page() {
useQuery({
queryKey: [key],
queryFn: async () => {
await sleep(10)
return key
},
suspense: true,
select: () => 'selected',
onSuccess: successFn,
})

return <>rendered</>
}

const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)

await waitFor(() => rendered.getByText('rendered'))

await waitFor(() => expect(successFn).toHaveBeenCalledTimes(1))
await waitFor(() => expect(successFn).toHaveBeenCalledWith('selected'))
})

it('should call every onSuccess handler within a suspense boundary', async () => {
const key = queryKey()

const successFn1 = vi.fn()
const successFn2 = vi.fn()

function FirstComponent() {
useQuery({
queryKey: key,
queryFn: () => {
sleep(10)
return 'data'
},
suspense: true,
onSuccess: successFn1,
})

return <span>first</span>
}

function SecondComponent() {
useQuery({
queryKey: key,
queryFn: () => {
sleep(10)
return 'data'
},

suspense: true,
onSuccess: successFn2,
})

return <span>second</span>
}

const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<FirstComponent />
<SecondComponent />
</React.Suspense>,
)

await waitFor(() => rendered.getByText('second'))

await waitFor(() => expect(successFn1).toHaveBeenCalledTimes(1))
await waitFor(() => expect(successFn2).toHaveBeenCalledTimes(1))
})

// https://github.com/tannerlinsley/react-query/issues/468
it('should reset error state if new component instances are mounted', async () => {
const consoleMock = vi
Expand Down
27 changes: 0 additions & 27 deletions packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,33 +128,6 @@ describe('select', () => {
return result
})
})
it('should pass transformed data to onSuccess', () => {
doNotExecute(() => {
const infiniteQuery = useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
return pageParam * 5
},
defaultPageParam: 1,
getNextPageParam: () => undefined,
select: (data) => {
return {
...data,
pages: data.pages.map((page) => page.toString()),
}
},
onSuccess: (data) => {
const result: Expect<Equal<InfiniteData<string>, typeof data>> = true
doNotExecute(() => result)
},
})

const result: Expect<
Equal<InfiniteData<string> | undefined, (typeof infiniteQuery)['data']>
> = true
return result
})
})
})
describe('getNextPageParam / getPreviousPageParam', () => {
it('should get typed params', () => {
Expand Down
Loading