diff --git a/docs/react/guides/migrating-to-v5.md b/docs/react/guides/migrating-to-v5.md index f232aac22e7..feb536f13c9 100644 --- a/docs/react/guides/migrating-to-v5.md +++ b/docs/react/guides/migrating-to-v5.md @@ -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. @@ -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. @@ -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`: diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index 50ab605ba0d..d0b84df7c27 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -158,13 +158,6 @@ export class QueriesObserver extends Subscribable { (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( diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index cadfef0a68b..22518efb50d 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -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 }) diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 33f837eef6b..63023f1840e 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -17,11 +17,11 @@ 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 = ( result: QueryObserverResult, @@ -29,8 +29,6 @@ type QueryObserverListener = ( export interface NotifyOptions { listeners?: boolean - onError?: boolean - onSuccess?: boolean } export interface ObserverFetchOptions extends FetchOptions { @@ -632,16 +630,8 @@ export class QueryObserver< } } - onQueryUpdate(action: Action): 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() @@ -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) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 59ef8d7eed8..d07fcb8e55a 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -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 | '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. diff --git a/packages/react-query/src/__tests__/suspense.test.tsx b/packages/react-query/src/__tests__/suspense.test.tsx index 794004dcc49..204a363c495 100644 --- a/packages/react-query/src/__tests__/suspense.test.tsx +++ b/packages/react-query/src/__tests__/suspense.test.tsx @@ -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, - - - , - ) - - 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 first - } - - function SecondComponent() { - useQuery({ - queryKey: key, - queryFn: () => { - sleep(10) - return 'data' - }, - - suspense: true, - onSuccess: successFn2, - }) - - return second - } - - const rendered = renderWithClient( - queryClient, - - - - , - ) - - 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 diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx index 1b40acffee6..73a4b63d0e4 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.type.test.tsx @@ -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, typeof data>> = true - doNotExecute(() => result) - }, - }) - - const result: Expect< - Equal | undefined, (typeof infiniteQuery)['data']> - > = true - return result - }) - }) }) describe('getNextPageParam / getPreviousPageParam', () => { it('should get typed params', () => { diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index f582c7698d4..4456a03c805 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -144,10 +144,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return a.toLowerCase() }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -160,14 +156,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return parseInt(a) }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, - onError: (e) => { - expectType(e) - expectTypeNotAny(e) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -304,10 +292,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return a.toLowerCase() }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -320,14 +304,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return parseInt(a) }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, - onError: (e) => { - expectType(e) - expectTypeNotAny(e) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -423,60 +399,38 @@ describe('useQueries', () => { ], }) - // select / onSuccess / onSettled params are "indirectly" enforced + // select params are "indirectly" enforced useQueries({ queries: [ // unfortunately TS will not suggest the type for you { queryKey: key1, queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: (a) => null, - // @ts-expect-error (noImplicitAny) - onSettled: (a) => null, }, // however you can add a type to the callback { queryKey: key2, queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, // the type you do pass is enforced { queryKey: key3, queryFn: () => 'string', - // @ts-expect-error (only accepts string) - onSuccess: (a: number) => null, }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), - // @ts-expect-error (select is defined => only accepts number) - onSuccess: (a: string) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, ], }) // callbacks are also indirectly enforced with Array.map useQueries({ - // @ts-expect-error (onSuccess only accepts string) queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), - onSuccess: (_data: number) => null, })), }) useQueries({ @@ -484,7 +438,6 @@ describe('useQueries', () => { queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), - onSuccess: (_data: string) => null, })), }) @@ -494,32 +447,15 @@ describe('useQueries', () => { { queryKey: key1, queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: (a) => null, - // @ts-expect-error (noImplicitAny) - onSettled: (a) => null, }, { queryKey: key2, queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), - onSuccess: (_a: number) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, ], }) @@ -533,12 +469,6 @@ describe('useQueries', () => { { queryKey: key1, queryFn: () => Promise.resolve('string'), - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - // @ts-expect-error (refuses to accept a Promise) - onSettled: (a: Promise) => null, }, ], }) @@ -645,11 +575,10 @@ describe('useQueries', () => { queries: queries.map( // no need to type the mapped query (query) => { - const { queryFn: fn, queryKey: key, onError: err } = query + const { queryFn: fn, queryKey: key } = query expectType | undefined>(fn) return { queryKey: key, - onError: err, queryFn: fn ? (ctx: QueryFunctionContext) => { expectType(ctx.queryKey) diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index eb0294438d7..3df0c0685bf 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -64,23 +64,17 @@ describe('useQuery', () => { useQuery({ queryKey: [key], queryFn: async () => true, - onSuccess: (data) => expectType(data), - onSettled: (data) => expectType(data), }) // it should be possible to specify a union type as result type const unionTypeSync = useQuery({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? 'a' : 'b'), - - onSuccess: (data) => expectType<'a' | 'b'>(data), }) expectType<'a' | 'b' | undefined>(unionTypeSync.data) const unionTypeAsync = useQuery<'a' | 'b'>({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), - - onSuccess: (data) => expectType<'a' | 'b'>(data), }) expectType<'a' | 'b' | undefined>(unionTypeAsync.data) @@ -450,255 +444,6 @@ describe('useQuery', () => { }) }) - it('should call onSuccess after a query has been fetched', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 'data' - }, - onSuccess, - }) - states.push(state) - return
data: {state.data}
- } - - const rendered = renderWithClient(queryClient, ) - - await rendered.findByText('data: data') - expect(states.length).toBe(2) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - - it('should call onSuccess after a query has been refetched', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onSuccess = vi.fn() - let count = 0 - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: async () => { - count++ - await sleep(10) - return 'data' + count - }, - onSuccess, - }) - - states.push(state) - - return ( -
-
data: {state.data}
- -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await rendered.findByText('data: data1') - fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) - await rendered.findByText('data: data2') - - expect(states.length).toBe(3) //pending, success, success after refetch - expect(count).toBe(2) - expect(onSuccess).toHaveBeenCalledTimes(2) - }) - - it('should call onSuccess after a disabled query has been fetched', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: () => 'data', - enabled: false, - onSuccess, - }) - - states.push(state) - - return ( -
-
isSuccess: {state.isSuccess ? 'true' : 'false'}
- -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => { - rendered.getByText('isSuccess: false') - }) - - fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) - - await waitFor(() => { - rendered.getByText('isSuccess: true') - }) - - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - - it('should not call onSuccess if a component has unmounted', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const [show, setShow] = React.useState(true) - - React.useEffect(() => { - setShow(false) - }, [setShow]) - - return show ? : null - } - - function Component() { - const state = useQuery({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 'data' - }, - onSuccess, - }) - states.push(state) - return null - } - - renderWithClient(queryClient, ) - - await sleep(50) - expect(states.length).toBe(1) - expect(onSuccess).toHaveBeenCalledTimes(0) - }) - - it('should call onError after a query has been fetched with an error', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onError = vi.fn() - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: () => Promise.reject(new Error('error')), - retry: false, - onError, - }) - states.push(state) - return null - } - - renderWithClient(queryClient, ) - - await sleep(10) - expect(states.length).toBe(2) - expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith(new Error('error')) - }) - - it('should not call onError when receiving a CancelledError', async () => { - const key = queryKey() - const onError = vi.fn() - - function Page() { - const { status, fetchStatus } = useQuery({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 23 - }, - - onError, - }) - return ( - - status: {status}, fetchStatus: {fetchStatus} - - ) - } - - const rendered = renderWithClient(queryClient, ) - - rendered.getByText('status: pending, fetchStatus: fetching') - - await queryClient.cancelQueries({ queryKey: key }) - // query cancellation will reset the query to it's initial state - await waitFor(() => - rendered.getByText('status: pending, fetchStatus: idle'), - ) - expect(onError).not.toHaveBeenCalled() - }) - - it('should call onSettled after a query has been fetched', async () => { - const key = queryKey() - const states: UseQueryResult[] = [] - const onSettled = vi.fn() - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: () => 'data', - onSettled, - }) - states.push(state) - - return
data: {state.data}
- } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => { - rendered.getByText('data: data') - }) - - expect(states.length).toBe(2) - expect(onSettled).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledWith('data', null) - }) - - it('should call onSettled after a query has been fetched with an error', async () => { - const key = queryKey() - const onSettled = vi.fn() - const error = new Error('error') - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return Promise.reject(error) - }, - retry: false, - onSettled, - }) - return
status: {state.status}
- } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => { - rendered.getByText('status: error') - }) - expect(onSettled).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledWith(undefined, error) - }) - it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => { const key = queryKey() let fetchCount = 0 @@ -2587,53 +2332,6 @@ describe('useQuery', () => { expect(renders).toBe(2) }) - it('should batch re-renders including hook callbacks', async () => { - const key = queryKey() - - let renders = 0 - let callbackCount = 0 - - const queryFn = async () => { - await sleep(10) - return 'data' - } - - function Page() { - const [count, setCount] = React.useState(0) - useQuery({ - queryKey: key, - queryFn, - onSuccess: () => { - setCount((x) => x + 1) - }, - }) - useQuery({ - queryKey: key, - queryFn, - onSuccess: () => { - setCount((x) => x + 1) - }, - }) - - React.useEffect(() => { - renders++ - callbackCount = count - }) - - return
count: {count}
- } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('count: 2')) - - // Should be 3 instead of 5 - expect(renders).toBe(3) - - // Both callbacks should have been executed - expect(callbackCount).toBe(2) - }) - it('should render latest data even if react has discarded certain renders', async () => { const key = queryKey() @@ -6126,36 +5824,6 @@ describe('useQuery', () => { }) }) - it('setQueryData - should not call onSuccess callback of active observers', async () => { - const key = queryKey() - const onSuccess = vi.fn() - - function Page() { - const state = useQuery({ - queryKey: key, - queryFn: () => 'data', - onSuccess, - }) - return ( -
-
data: {state.data}
- -
- ) - } - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => rendered.getByText('data: data')) - fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) - await waitFor(() => rendered.getByText('data: newData')) - - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - it('setQueryData - should respect updatedAt', async () => { const key = queryKey() diff --git a/packages/react-query/src/suspense.ts b/packages/react-query/src/suspense.ts index 682409e75d7..ce20199de42 100644 --- a/packages/react-query/src/suspense.ts +++ b/packages/react-query/src/suspense.ts @@ -46,14 +46,6 @@ export const fetchOptimistic = < observer: QueryObserver, errorResetBoundary: QueryErrorResetBoundaryValue, ) => - observer - .fetchOptimistic(defaultedOptions) - .then(({ data }) => { - defaultedOptions.onSuccess?.(data as TData) - defaultedOptions.onSettled?.(data, null) - }) - .catch((error) => { - errorResetBoundary.clearReset() - defaultedOptions.onError?.(error) - defaultedOptions.onSettled?.(undefined, error) - }) + observer.fetchOptimistic(defaultedOptions).catch(() => { + errorResetBoundary.clearReset() + }) diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index 6074fde1652..d7aefdeba95 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -41,25 +41,6 @@ export function useBaseQuery< ? 'isRestoring' : 'optimistic' - // Include callbacks in batch renders - if (defaultedOptions.onError) { - defaultedOptions.onError = notifyManager.batchCalls( - defaultedOptions.onError, - ) - } - - if (defaultedOptions.onSuccess) { - defaultedOptions.onSuccess = notifyManager.batchCalls( - defaultedOptions.onSuccess, - ) - } - - if (defaultedOptions.onSettled) { - defaultedOptions.onSettled = notifyManager.batchCalls( - defaultedOptions.onSettled, - ) - } - ensureStaleTime(defaultedOptions) ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary) diff --git a/packages/solid-query/src/__tests__/createQueries.test.tsx b/packages/solid-query/src/__tests__/createQueries.test.tsx index db16ceb6cbb..c301b480348 100644 --- a/packages/solid-query/src/__tests__/createQueries.test.tsx +++ b/packages/solid-query/src/__tests__/createQueries.test.tsx @@ -158,10 +158,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return a.toLowerCase() }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -174,14 +170,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return parseInt(a) }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, - onError: (e) => { - expectType(e) - expectTypeNotAny(e) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -319,10 +307,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return a.toLowerCase() }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -335,14 +319,6 @@ describe('useQueries', () => { expectTypeNotAny(a) return parseInt(a) }, - onSuccess: (a) => { - expectType(a) - expectTypeNotAny(a) - }, - onError: (e) => { - expectType(e) - expectTypeNotAny(e) - }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, @@ -436,60 +412,38 @@ describe('useQueries', () => { ], })) - // select / onSuccess / onSettled params are "indirectly" enforced + // select params are "indirectly" enforced createQueries(() => ({ queries: [ // unfortunately TS will not suggest the type for you { queryKey: key1, queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: (a) => null, - // @ts-expect-error (noImplicitAny) - onSettled: (a) => null, }, // however you can add a type to the callback { queryKey: key2, queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, // the type you do pass is enforced { queryKey: key3, queryFn: () => 'string', - // @ts-expect-error (only accepts string) - onSuccess: (a: number) => null, }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), - // @ts-expect-error (select is defined => only accepts number) - onSuccess: (a: string) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, ], })) // callbacks are also indirectly enforced with Array.map createQueries(() => ({ - // @ts-expect-error (onSuccess only accepts string) queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), - onSuccess: (_data: number) => null, })), })) @@ -498,7 +452,6 @@ describe('useQueries', () => { queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), - onSuccess: (_data: string) => null, })), })) @@ -508,32 +461,15 @@ describe('useQueries', () => { { queryKey: key1, queryFn: () => 'string', - // @ts-expect-error (noImplicitAny) - onSuccess: (a) => null, - // @ts-expect-error (noImplicitAny) - onSettled: (a) => null, }, { queryKey: key2, queryFn: () => 'string', - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - onSettled: (a: string | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), - onSuccess: (_a: number) => null, - onSettled: (a: number | undefined) => { - expectType(a) - expectTypeNotAny(a) - }, }, ], })) @@ -547,12 +483,6 @@ describe('useQueries', () => { { queryKey: key1, queryFn: () => Promise.resolve('string'), - onSuccess: (a: string) => { - expectType(a) - expectTypeNotAny(a) - }, - // @ts-expect-error (refuses to accept a Promise) - onSettled: (a: Promise) => null, }, ], })) @@ -658,11 +588,10 @@ describe('useQueries', () => { queries: queries.map( // no need to type the mapped query (query) => { - const { queryFn: fn, queryKey: key, onError: err } = query + const { queryFn: fn, queryKey: key } = query expectType | undefined>(fn) return { queryKey: key, - onError: err, queryFn: fn ? (ctx: QueryFunctionContext) => { expectType(ctx.queryKey) diff --git a/packages/solid-query/src/__tests__/createQuery.test.tsx b/packages/solid-query/src/__tests__/createQuery.test.tsx index d9855531e9a..8a7948766d6 100644 --- a/packages/solid-query/src/__tests__/createQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createQuery.test.tsx @@ -80,21 +80,17 @@ describe('createQuery', () => { createQuery(() => ({ queryKey: [key], queryFn: async () => true, - onSuccess: (data) => expectType(data), - onSettled: (data) => expectType(data), })) // it should be possible to specify a union type as result type const unionTypeSync = createQuery(() => ({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? 'a' : 'b'), - onSuccess: (data) => expectType<'a' | 'b'>(data), })) expectType<'a' | 'b' | undefined>(unionTypeSync.data) const unionTypeAsync = createQuery<'a' | 'b'>(() => ({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), - onSuccess: (data) => expectType<'a' | 'b'>(data), })) expectType<'a' | 'b' | undefined>(unionTypeAsync.data) @@ -491,241 +487,6 @@ describe('createQuery', () => { }) }) - it('should call onSuccess after a query has been fetched', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 'data' - }, - onSuccess, - })) - createRenderEffect(() => { - states.push({ ...state }) - }) - return
data: {state.data}
- } - - render(() => ( - - - - )) - - await screen.findByText('data: data') - expect(states.length).toBe(2) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - - it('should call onSuccess after a disabled query has been fetched', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: () => 'data', - enabled: false, - onSuccess, - })) - - createRenderEffect(() => { - states.push({ ...state }) - }) - - createEffect(() => { - const refetch = state.refetch - setActTimeout(() => { - refetch() - }, 10) - }) - - return null - } - - render(() => ( - - - - )) - - await sleep(50) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - - it('should not call onSuccess if a component has unmounted', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onSuccess = vi.fn() - - function Page() { - const [show, setShow] = createSignal(true) - - createEffect(() => { - setShow(false) - }) - return <>{show() && } - } - - function Component() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 'data' - }, - onSuccess, - })) - createRenderEffect(() => { - states.push({ ...state }) - }) - return null - } - - render(() => ( - - - - )) - - await sleep(50) - expect(states.length).toBe(1) - expect(onSuccess).toHaveBeenCalledTimes(0) - }) - - it('should call onError after a query has been fetched with an error', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onError = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: () => Promise.reject(new Error('error')), - retry: false, - onError, - })) - - createRenderEffect(() => { - states.push({ ...state }) - }) - - return null - } - - render(() => ( - - - - )) - - await sleep(10) - expect(states.length).toBe(2) - expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith(new Error('error')) - }) - - it('should not call onError when receiving a CancelledError', async () => { - const key = queryKey() - const onError = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: async () => { - await sleep(10) - return 23 - }, - onError, - })) - return ( - - status: {state.status}, fetchStatus: {state.fetchStatus} - - ) - } - - render(() => ( - - - - )) - - await sleep(5) - await queryClient.cancelQueries({ queryKey: key }) - // query cancellation will reset the query to it's initial state - await waitFor(() => screen.getByText('status: pending, fetchStatus: idle')) - expect(onError).not.toHaveBeenCalled() - }) - - it('should call onSettled after a query has been fetched', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onSettled = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: () => 'data', - onSettled, - })) - - createRenderEffect(() => { - states.push({ ...state }) - }) - return null - } - - render(() => ( - - - - )) - - await sleep(10) - expect(states.length).toBe(2) - expect(onSettled).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledWith('data', null) - }) - - it('should call onSettled after a query has been fetched with an error', async () => { - const key = queryKey() - const states: CreateQueryResult[] = [] - const onSettled = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: () => Promise.reject('error'), - retry: false, - onSettled, - })) - createRenderEffect(() => { - states.push({ ...state }) - }) - return null - } - - render(() => ( - - - - )) - - await sleep(10) - expect(states.length).toBe(2) - expect(onSettled).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledWith(undefined, 'error') - }) - it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => { const key = queryKey() let fetchCount = 0 @@ -2274,56 +2035,6 @@ describe('createQuery', () => { expect(renders).toBe(1) }) - it('should batch re-renders including hook callbacks', async () => { - const key = queryKey() - - let renders = 0 - let callbackCount = 0 - - const queryFn = async () => { - await sleep(10) - return 'data' - } - - function Page() { - const [count, setCount] = createSignal(0) - createQuery(() => ({ - queryKey: key, - queryFn, - onSuccess: () => { - setCount((x) => x + 1) - }, - })) - createQuery(() => ({ - queryKey: key, - queryFn, - onSuccess: () => { - setCount((x) => x + 1) - }, - })) - - createEffect(() => { - renders++ - callbackCount = count() - }) - - return
count: {count()}
- } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('count: 2')) - - // Should be 3 instead of 5 - expect(renders).toBe(3) - // Both callbacks should have been executed - expect(callbackCount).toBe(2) - }) - it('should render latest data even if react has discarded certain renders', async () => { const key = queryKey() @@ -6107,40 +5818,6 @@ describe('createQuery', () => { }) }) - it('setQueryData - should not call onSuccess callback of active observers', async () => { - const key = queryKey() - const onSuccess = vi.fn() - - function Page() { - const state = createQuery(() => ({ - queryKey: key, - queryFn: () => 'data', - onSuccess, - })) - return ( -
-
data: {state.data}
- -
- ) - } - - render(() => ( - - - - )) - - await waitFor(() => screen.getByText('data: data')) - fireEvent.click(screen.getByRole('button', { name: /setQueryData/i })) - await waitFor(() => screen.getByText('data: newData')) - - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith('data') - }) - it('setQueryData - should respect updatedAt', async () => { const key = queryKey() diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index e50afce71d0..b905b2d4855 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -217,89 +217,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() { - createQuery(() => ({ - queryKey: [key], - queryFn: async () => { - await sleep(10) - return key - }, - - suspense: true, - select: () => 'selected', - onSuccess: successFn, - })) - - return <>rendered - } - - render(() => ( - - - - - - )) - - await waitFor(() => screen.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() { - createQuery(() => ({ - queryKey: key, - queryFn: () => { - sleep(10) - return 'data' - }, - onSuccess: successFn1, - })) - - return first - } - - function SecondComponent() { - createQuery(() => ({ - queryKey: key, - queryFn: () => { - sleep(10) - return 'data' - }, - onSuccess: successFn2, - })) - - return second - } - - render(() => ( - - - - - - , - - )) - - await waitFor(() => screen.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 key = queryKey() diff --git a/packages/svelte-query/src/createBaseQuery.ts b/packages/svelte-query/src/createBaseQuery.ts index 64f1d9d1fd1..430e2efb317 100644 --- a/packages/svelte-query/src/createBaseQuery.ts +++ b/packages/svelte-query/src/createBaseQuery.ts @@ -30,25 +30,6 @@ export function createBaseQuery< const defaultedOptions = client.defaultQueryOptions($options) defaultedOptions._optimisticResults = 'optimistic' - // Include callbacks in batch renders - if (defaultedOptions.onError) { - defaultedOptions.onError = notifyManager.batchCalls( - defaultedOptions.onError, - ) - } - - if (defaultedOptions.onSuccess) { - defaultedOptions.onSuccess = notifyManager.batchCalls( - defaultedOptions.onSuccess, - ) - } - - if (defaultedOptions.onSettled) { - defaultedOptions.onSettled = notifyManager.batchCalls( - defaultedOptions.onSettled, - ) - } - return defaultedOptions }) diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts index aea379434f9..a88b64fd2bb 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -124,27 +124,6 @@ describe('useQuery', () => { }) }) - test('should update query on reactive options object change', async () => { - const spy = vi.fn() - const onSuccess = ref(() => { - // Noop - }) - useQuery( - reactive({ - queryKey: ['key6'], - queryFn: simpleFetcher, - onSuccess, - staleTime: 1000, - }), - ) - - onSuccess.value = spy - - await flushPromises() - - expect(spy).toBeCalledTimes(1) - }) - test('should update query on reactive (Ref) key change', async () => { const secondKeyRef = ref('key7') const query = useQuery({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df8bdfac125..f085fee11ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -720,12 +720,12 @@ importers: '@tanstack/solid-query': link:../../../packages/solid-query solid-js: 1.6.16 solid-start: 0.2.24_euxzheln45tfyr565dhf2xv3la - undici: 5.14.0 + undici: 5.20.0 devDependencies: '@types/node': 18.13.0 esbuild: 0.14.54 postcss: 8.4.21 - solid-start-node: 0.2.21_2vqkbilyolq35n53mildrhalmu + solid-start-node: 0.2.21_wmplqqqr5tdglivwyrjsogoe24 typescript: 4.9.5 vite: 4.2.1_@types+node@18.13.0 @@ -1854,7 +1854,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.3 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.19.1: resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} @@ -17101,7 +17101,7 @@ packages: '@babel/types': 7.21.3 solid-js: 1.6.16 - /solid-start-node/0.2.21_2vqkbilyolq35n53mildrhalmu: + /solid-start-node/0.2.21_wmplqqqr5tdglivwyrjsogoe24: resolution: {integrity: sha512-DmmJT6K+uF0yLPkn1GQvIHmZRUdYlQwBijruvCiEWuy3d1sLedRnP22K/+u9eKqm8ruFOTQHrc/1wuGVcj3JXg==} peerDependencies: solid-start: '*' @@ -17117,7 +17117,7 @@ packages: sirv: 2.0.2 solid-start: 0.2.24_euxzheln45tfyr565dhf2xv3la terser: 5.16.3 - undici: 5.14.0 + undici: 5.20.0 vite: 4.2.1_@types+node@18.13.0 transitivePeerDependencies: - supports-color @@ -17185,9 +17185,9 @@ packages: set-cookie-parser: 2.5.1 sirv: 2.0.2 solid-js: 1.6.16 - solid-start-node: 0.2.21_2vqkbilyolq35n53mildrhalmu + solid-start-node: 0.2.21_wmplqqqr5tdglivwyrjsogoe24 terser: 5.16.3 - undici: 5.19.1 + undici: 5.20.0 vite: 4.2.1_@types+node@18.13.0 vite-plugin-inspect: 0.7.15_rollup@3.20.2+vite@4.2.1 vite-plugin-solid: 2.6.1_solid-js@1.6.16+vite@4.2.1 @@ -18338,12 +18338,6 @@ packages: dependencies: busboy: 1.6.0 - /undici/5.19.1: - resolution: {integrity: sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==} - engines: {node: '>=12.18'} - dependencies: - busboy: 1.6.0 - /unfetch/4.1.0: resolution: {integrity: sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==} dev: false