From 39d59e40a66b2b13564277f215cbc9544c22e772 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 18 Aug 2026 11:56:56 +0200 Subject: [PATCH] ref: remove experimental_prefetchInRender --- .changeset/quiet-flags-remove.md | 12 + docs/framework/react/guides/suspense.md | 54 - .../framework/react/reference/queryOptions.md | 6 - .../react/reference/useInfiniteQuery.md | 8 - docs/framework/react/reference/useQuery.md | 4 - .../src/createInfiniteQueryController.ts | 1 - .../lit-query/src/createQueriesController.ts | 2 - .../lit-query/src/createQueryController.ts | 1 - .../src/__tests__/useInfiniteQuery.test.tsx | 7 - .../src/__tests__/useQuery.test-d.tsx | 2 +- .../src/__tests__/useQuery.test.tsx | 10 - .../preact-query/src/errorBoundaryUtils.ts | 6 +- packages/preact-query/src/suspense.ts | 5 - packages/preact-query/src/types.ts | 4 +- packages/preact-query/src/useBaseQuery.ts | 25 +- .../src/__tests__/queryObserver.test.tsx | 141 +- .../src/__tests__/thenable.test.tsx | 134 -- packages/query-core/src/hydration.ts | 20 +- packages/query-core/src/queryObserver.ts | 63 - packages/query-core/src/retryer.ts | 32 +- packages/query-core/src/thenable.ts | 118 -- packages/query-core/src/types.ts | 54 - .../src/__tests__/useInfiniteQuery.test.tsx | 97 +- .../src/__tests__/useQuery.promise.test.tsx | 1453 ----------------- .../src/__tests__/useQuery.test-d.tsx | 2 +- .../src/__tests__/useQuery.test.tsx | 10 - .../src/__tests__/useSuspenseQuery.test.tsx | 5 - .../react-query/src/errorBoundaryUtils.ts | 6 +- packages/react-query/src/suspense.ts | 5 - packages/react-query/src/types.ts | 4 +- packages/react-query/src/useBaseQuery.ts | 25 +- .../src/__tests__/useInfiniteQuery.test.tsx | 2 - .../src/__tests__/useQuery.test.tsx | 5 - packages/solid-query/src/useBaseQuery.ts | 3 - .../createInfiniteQuery.svelte.test.ts | 2 - .../createQuery/createQuery.svelte.test.ts | 9 - 36 files changed, 63 insertions(+), 2274 deletions(-) create mode 100644 .changeset/quiet-flags-remove.md delete mode 100644 packages/query-core/src/__tests__/thenable.test.tsx delete mode 100644 packages/query-core/src/thenable.ts delete mode 100644 packages/react-query/src/__tests__/useQuery.promise.test.tsx diff --git a/.changeset/quiet-flags-remove.md b/.changeset/quiet-flags-remove.md new file mode 100644 index 00000000000..960b5659c95 --- /dev/null +++ b/.changeset/quiet-flags-remove.md @@ -0,0 +1,12 @@ +--- +'@tanstack/angular-query-experimental': patch +'@tanstack/lit-query': patch +'@tanstack/preact-query': patch +'@tanstack/query-core': patch +'@tanstack/react-query': patch +'@tanstack/solid-query': patch +'@tanstack/svelte-query': patch +'@tanstack/vue-query': patch +--- + +Remove experimental render-time prefetching and the `promise` property from query results. diff --git a/docs/framework/react/guides/suspense.md b/docs/framework/react/guides/suspense.md index 7c48e65a14c..2c84c5ba308 100644 --- a/docs/framework/react/guides/suspense.md +++ b/docs/framework/react/guides/suspense.md @@ -8,7 +8,6 @@ React Query can also be used with React's Suspense for Data Fetching APIs. For t - [useSuspenseQuery](../reference/useSuspenseQuery.md) - [useSuspenseInfiniteQuery](../reference/useSuspenseInfiniteQuery.md) - [useSuspenseQueries](../reference/useSuspenseQueries.md) -- Additionally, you can use the `useQuery().promise` and `React.use()` (Experimental) When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](../examples/suspense) for more information on how to set up suspense mode. @@ -173,56 +172,3 @@ export function Providers(props: { children: React.ReactNode }) { ``` For more information, check out the [NextJs Suspense Streaming Example](../examples/nextjs-suspense-streaming) and the [Advanced Rendering & Hydration](./advanced-ssr.md) guide. - -## Using `useQuery().promise` and `React.use()` (Experimental) - -> To enable this feature, you need to set the `experimental_prefetchInRender` option to `true` when creating your `QueryClient` - -**Example code:** - -```tsx -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, -}) -``` - -**Usage:** - -```tsx -import React from 'react' -import { useQuery } from '@tanstack/react-query' -import { fetchTodos, type Todo } from './api' - -function TodoList({ query }: { query: UseQueryResult }) { - const data = React.use(query.promise) - - return ( - - ) -} - -export function App() { - const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) - - return ( - <> -

Todos

- Loading...}> - - - - ) -} -``` - -For a more complete example, see [suspense example on GitHub](https://github.com/TanStack/query/tree/main/examples/react/suspense). - -For a Next.js streaming example, see [nextjs-suspense-streaming example on GitHub](https://github.com/TanStack/query/tree/main/examples/react/nextjs-suspense-streaming). diff --git a/docs/framework/react/reference/queryOptions.md b/docs/framework/react/reference/queryOptions.md index b6c5409372d..6863da9aae2 100644 --- a/docs/framework/react/reference/queryOptions.md +++ b/docs/framework/react/reference/queryOptions.md @@ -17,12 +17,6 @@ You can generally pass everything to `queryOptions` that you can also pass to [` - `queryKey: QueryKey` - **Required** - The query key to generate options for. -- `experimental_prefetchInRender?: boolean` - - Optional - - Defaults to `false` - - When set to `true`, queries will be prefetched during render, which can be useful for certain optimization scenarios - - Needs to be turned on for the experimental `useQuery().promise` functionality - [//]: # 'Materials' ## Further reading diff --git a/docs/framework/react/reference/useInfiniteQuery.md b/docs/framework/react/reference/useInfiniteQuery.md index 8370b7ac1d9..0a81d0a4be4 100644 --- a/docs/framework/react/reference/useInfiniteQuery.md +++ b/docs/framework/react/reference/useInfiniteQuery.md @@ -11,7 +11,6 @@ const { hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, - promise, ...result } = useInfiniteQuery({ queryKey, @@ -86,11 +85,4 @@ The returned properties for `useInfiniteQuery` are identical to the [`useQuery` - Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage` - `isRefetchError: boolean` - Will be `true` if the query failed while refetching a page. -- `promise: Promise` - - A stable promise that resolves to the query result. - [//]: # 'ReactUse' - - This can be used with `React.use()` to fetch data - [//]: # 'ReactUse' - - Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`. - Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default refetch behaviour, resulting in outdated data. Make sure to call these functions only in response to user actions, or add conditions like `hasNextPage && !isFetching`. diff --git a/docs/framework/react/reference/useQuery.md b/docs/framework/react/reference/useQuery.md index 55f4f99334f..10f6df3aeeb 100644 --- a/docs/framework/react/reference/useQuery.md +++ b/docs/framework/react/reference/useQuery.md @@ -28,7 +28,6 @@ const { isStale, isSuccess, isEnabled, - promise, refetch, status, } = useQuery( @@ -257,6 +256,3 @@ const { - Defaults to `true` - Per default, a currently running request will be cancelled before a new request is made - When set to `false`, no refetch will be made if there is already a request running. -- `promise: Promise` - - A stable promise that will be resolved with the data of the query. - - Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`. diff --git a/packages/lit-query/src/createInfiniteQueryController.ts b/packages/lit-query/src/createInfiniteQueryController.ts index 6a41bb68371..973ac5befff 100644 --- a/packages/lit-query/src/createInfiniteQueryController.ts +++ b/packages/lit-query/src/createInfiniteQueryController.ts @@ -109,7 +109,6 @@ function createPendingInfiniteQueryResult< isFetchingNextPage: false, isFetchPreviousPageError: false, isFetchingPreviousPage: false, - promise: Promise.resolve(undefined as never), } as unknown as InfiniteQueryObserverResult } diff --git a/packages/lit-query/src/createQueriesController.ts b/packages/lit-query/src/createQueriesController.ts index 2af3b312004..297f8152c92 100644 --- a/packages/lit-query/src/createQueriesController.ts +++ b/packages/lit-query/src/createQueriesController.ts @@ -251,7 +251,6 @@ function createPendingQueryObserverResult(): QueryObserverResult { Promise.reject( createMissingQueryClientError(), )) as QueryObserverResult['refetch'], - promise: Promise.resolve(undefined as never), } as unknown as QueryObserverResult } @@ -282,7 +281,6 @@ function createPlaceholderQueryObserverResult( isLoading: false, isSuccess: true, status: 'success', - promise: Promise.resolve(data as never), } as QueryObserverResult } diff --git a/packages/lit-query/src/createQueryController.ts b/packages/lit-query/src/createQueryController.ts index 09ec7af1391..dfec717b74b 100644 --- a/packages/lit-query/src/createQueryController.ts +++ b/packages/lit-query/src/createQueryController.ts @@ -84,7 +84,6 @@ function createPendingQueryResult(): QueryObserverResult< TData, TError >['refetch'], - promise: Promise.resolve(undefined as never), } as unknown as QueryObserverResult } diff --git a/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx index 55601b5c952..74ac74d5124 100644 --- a/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useInfiniteQuery.test.tsx @@ -38,11 +38,6 @@ describe('useInfiniteQuery', () => { queryCache = new QueryCache() queryClient = new QueryClient({ queryCache, - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, }) }) @@ -105,7 +100,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, @@ -141,7 +135,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) diff --git a/packages/preact-query/src/__tests__/useQuery.test-d.tsx b/packages/preact-query/src/__tests__/useQuery.test-d.tsx index 55461084db7..a663531d215 100644 --- a/packages/preact-query/src/__tests__/useQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useQuery.test-d.tsx @@ -17,7 +17,7 @@ describe('useQuery', () => { const fromQueryFn = useQuery({ queryKey: key, queryFn: () => 'test' }) expectTypeOf(fromQueryFn.data).toEqualTypeOf() expectTypeOf(fromQueryFn.error).toEqualTypeOf() - expectTypeOf(fromQueryFn.promise).toEqualTypeOf>() + expectTypeOf(fromQueryFn).not.toHaveProperty('promise') // it should be possible to specify the result type const withResult = useQuery({ diff --git a/packages/preact-query/src/__tests__/useQuery.test.tsx b/packages/preact-query/src/__tests__/useQuery.test.tsx index 3a36725ff09..537632addfd 100644 --- a/packages/preact-query/src/__tests__/useQuery.test.tsx +++ b/packages/preact-query/src/__tests__/useQuery.test.tsx @@ -132,7 +132,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -161,10 +160,7 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) - - expect(states[0]!.promise).toEqual(states[1]!.promise) }) it('should return the correct states for an unsuccessful query', async () => { @@ -224,7 +220,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -253,7 +248,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[2]).toEqual({ @@ -282,11 +276,7 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', - promise: expect.any(Promise), }) - - expect(states[0]!.promise).toEqual(states[1]!.promise) - expect(states[1]!.promise).toEqual(states[2]!.promise) }) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { diff --git a/packages/preact-query/src/errorBoundaryUtils.ts b/packages/preact-query/src/errorBoundaryUtils.ts index 0b3d561fea6..aba60cc7b68 100644 --- a/packages/preact-query/src/errorBoundaryUtils.ts +++ b/packages/preact-query/src/errorBoundaryUtils.ts @@ -26,11 +26,7 @@ export const ensurePreventErrorBoundaryRetry = < >, errorResetBoundary: QueryErrorResetBoundaryValue, ) => { - if ( - options.suspense || - options.throwOnError || - options.experimental_prefetchInRender - ) { + if (options.suspense || options.throwOnError) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false diff --git a/packages/preact-query/src/suspense.ts b/packages/preact-query/src/suspense.ts index 8e1fa826584..3a8ed9e94ad 100644 --- a/packages/preact-query/src/suspense.ts +++ b/packages/preact-query/src/suspense.ts @@ -47,11 +47,6 @@ export const ensureSuspenseTimers = ( } } -export const willFetch = ( - result: QueryObserverResult, - isRestoring: boolean, -) => result.isLoading && result.isFetching && !isRestoring - export const shouldSuspend = ( defaultedOptions: | DefaultedQueryObserverOptions diff --git a/packages/preact-query/src/types.ts b/packages/preact-query/src/types.ts index 135af5a5261..543fb085073 100644 --- a/packages/preact-query/src/types.ts +++ b/packages/preact-query/src/types.ts @@ -162,7 +162,7 @@ export type UseSuspenseQueryResult< TError = DefaultError, > = DistributiveOmit< DefinedQueryObserverResult, - 'isPlaceholderData' | 'promise' + 'isPlaceholderData' > export type DefinedUseQueryResult< @@ -185,7 +185,7 @@ export type UseSuspenseInfiniteQueryResult< TError = DefaultError, > = OmitKeyof< DefinedInfiniteQueryObserverResult, - 'isPlaceholderData' | 'promise' + 'isPlaceholderData' > export type AnyUseMutationOptions = UseMutationOptions diff --git a/packages/preact-query/src/useBaseQuery.ts b/packages/preact-query/src/useBaseQuery.ts index 05ce46e7f6e..a32060460d4 100644 --- a/packages/preact-query/src/useBaseQuery.ts +++ b/packages/preact-query/src/useBaseQuery.ts @@ -1,4 +1,4 @@ -import { environmentManager, noop, notifyManager } from '@tanstack/query-core' +import { noop, notifyManager } from '@tanstack/query-core' import type { QueryClient, QueryKey, @@ -19,7 +19,6 @@ import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, - willFetch, } from './suspense' import type { UseBaseQueryOptions } from './types' import { useSyncExternalStore } from './utils' @@ -76,11 +75,6 @@ export function useBaseQuery< useClearResetErrorBoundary(errorResetBoundary) - // this needs to be invoked before creating the Observer because that can create a cache entry - const isNewCacheEntry = !client - .getQueryCache() - .get(defaultedOptions.queryHash) - const [observer] = useState( () => new Observer( @@ -145,23 +139,6 @@ export function useBaseQuery< result, ) - if ( - defaultedOptions.experimental_prefetchInRender && - !environmentManager.isServer() && - willFetch(result, isRestoring) - ) { - const promise = isNewCacheEntry - ? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted - fetchOptimistic(defaultedOptions, observer, errorResetBoundary) - : // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in - client.getQueryCache().get(defaultedOptions.queryHash)?.promise - - promise?.catch(noop).finally(() => { - // `.updateResult()` will trigger `.#currentThenable` to finalize - observer.updateResult() - }) - } - // Handle result property usage tracking return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index 08fac746a41..f3cc66c2609 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -16,13 +16,7 @@ describe('queryObserver', () => { beforeEach(() => { vi.useFakeTimers() - queryClient = new QueryClient({ - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, - }) + queryClient = new QueryClient() queryClient.mount() }) @@ -1463,96 +1457,6 @@ describe('queryObserver', () => { unsubscribe() }) - it('should return a promise that resolves when data is present', async () => { - const results: Array = [] - const key = queryKey() - let count = 0 - const observer = new QueryObserver(queryClient, { - queryKey: key, - queryFn: () => { - if (++count > 9) { - return Promise.resolve('data') - } - throw new Error('rejected') - }, - retry: 10, - retryDelay: 0, - }) - const unsubscribe = observer.subscribe(() => { - results.push(observer.getCurrentResult()) - }) - - await vi.advanceTimersByTimeAsync(8) - expect(results.at(-1)?.data).toBe('data') - - const numberOfUniquePromises = new Set( - results.map((result) => result.promise), - ).size - expect(numberOfUniquePromises).toBe(1) - - unsubscribe() - }) - - it('should return a new promise after recovering from an error', async () => { - const results: Array = [] - const key = queryKey() - - let succeeds = false - let idx = 0 - const observer = new QueryObserver(queryClient, { - queryKey: key, - queryFn: () => { - if (succeeds) { - return Promise.resolve('data') - } - throw new Error(`rejected #${++idx}`) - }, - retry: 5, - retryDelay: 0, - }) - const unsubscribe = observer.subscribe(() => { - results.push(observer.getCurrentResult()) - }) - - await vi.advanceTimersByTimeAsync(4) - expect(results.at(-1)?.status).toBe('error') - - expect( - results.every((result) => result.promise === results[0]!.promise), - ).toBe(true) - - { - // fail again - const lengthBefore = results.length - observer.refetch() - await vi.advanceTimersByTimeAsync(4) - expect(results.length).toBeGreaterThan(lengthBefore) - expect(results.at(-1)?.status).toBe('error') - - const numberOfUniquePromises = new Set( - results.map((result) => result.promise), - ).size - - expect(numberOfUniquePromises).toBe(2) - } - { - // succeed - succeeds = true - observer.refetch() - - await vi.advanceTimersByTimeAsync(0) - results.at(-1)?.status === 'success' - - const numberOfUniquePromises = new Set( - results.map((result) => result.promise), - ).size - - expect(numberOfUniquePromises).toBe(3) - } - - unsubscribe() - }) - it('should return true from shouldFetchOnWindowFocus when refetchOnWindowFocus is true', () => { const key = queryKey() @@ -1811,49 +1715,6 @@ describe('queryObserver', () => { unsubscribeSecond() }) - it('should reject promise when experimental_prefetchInRender is disabled and thenable is pending', async () => { - const key = queryKey() - const queryClient2 = new QueryClient({ - defaultOptions: { - queries: { - experimental_prefetchInRender: false, - }, - }, - }) - const observer = new QueryObserver(queryClient2, { - queryKey: key, - queryFn: () => sleep(10).then(() => 'data'), - enabled: false, - }) - - const result = observer.getCurrentResult() - const tracked = observer.trackResult(result) - - await expect(tracked.promise).rejects.toThrow( - 'experimental_prefetchInRender feature flag is not enabled', - ) - - queryClient2.clear() - }) - - it('should not reject promise when experimental_prefetchInRender is enabled', async () => { - const key = queryKey() - const observer = new QueryObserver(queryClient, { - queryKey: key, - queryFn: () => sleep(10).then(() => 'data'), - }) - - const unsubscribe = observer.subscribe(() => undefined) - const tracked = observer.trackResult(observer.getCurrentResult()) - const promise = tracked.promise - - await vi.advanceTimersByTimeAsync(10) - - await expect(promise).resolves.toBe('data') - - unsubscribe() - }) - it('should not refetchOnMount when set to "always" when staleTime is Static', async () => { const key = queryKey() const queryFn = vi.fn(() => 'data') diff --git a/packages/query-core/src/__tests__/thenable.test.tsx b/packages/query-core/src/__tests__/thenable.test.tsx deleted file mode 100644 index c852d507e79..00000000000 --- a/packages/query-core/src/__tests__/thenable.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { pendingThenable, tryResolveSync } from '../thenable' -import type { FulfilledThenable, RejectedThenable } from '../thenable' - -describe('pendingThenable', () => { - it('should start out pending with resolve and reject attached', () => { - const thenable = pendingThenable() - - expect(thenable.status).toBe('pending') - expect(typeof thenable.resolve).toBe('function') - expect(typeof thenable.reject).toBe('function') - }) - - it('should expose the value and drop the pending props once resolved', async () => { - const thenable = pendingThenable() - - thenable.resolve('data') - - expect(thenable.status).toBe('fulfilled') - expect((thenable as unknown as FulfilledThenable).value).toBe( - 'data', - ) - expect(thenable.resolve).toBeUndefined() - expect(thenable.reject).toBeUndefined() - await expect(thenable).resolves.toBe('data') - }) - - it('should expose the reason and drop the pending props once rejected', async () => { - const thenable = pendingThenable() - const reason = new Error('error') - - thenable.reject(reason) - - expect(thenable.status).toBe('rejected') - expect((thenable as unknown as RejectedThenable).reason).toBe( - reason, - ) - expect(thenable.resolve).toBeUndefined() - expect(thenable.reject).toBeUndefined() - await expect(thenable).rejects.toBe(reason) - }) - - it('should not reject a second time after being resolved', async () => { - const thenable = pendingThenable() - - thenable.resolve('data') - // `reject` is deleted on finalize, so a late caller cannot settle it twice - expect(thenable.reject).toBeUndefined() - - await expect(thenable).resolves.toBe('data') - }) - - it('should ignore a retained settlement callback invoked after it settled', async () => { - const thenable = pendingThenable() - // a caller can hold on to `reject` before the thenable settles - const retainedReject = thenable.reject - - thenable.resolve('data') - retainedReject(new Error('error')) - - expect(thenable.status).toBe('fulfilled') - expect((thenable as unknown as FulfilledThenable).value).toBe( - 'data', - ) - expect( - (thenable as unknown as RejectedThenable).reason, - ).toBeUndefined() - await expect(thenable).resolves.toBe('data') - }) - - it('should not report an unhandled rejection when nobody awaits it', async () => { - const onUnhandledRejection = vi.fn() - process.on('unhandledRejection', onUnhandledRejection) - - pendingThenable().reject(new Error('error')) - // unhandled rejections are reported after the microtask queue drains - await new Promise((resolve) => setTimeout(resolve, 0)) - - process.off('unhandledRejection', onUnhandledRejection) - expect(onUnhandledRejection).not.toHaveBeenCalled() - }) -}) - -describe('tryResolveSync', () => { - it('should return undefined for an already resolved native promise', () => { - // `then` on a native promise always defers to a microtask, so the data is - // never synchronously available - expect(tryResolveSync(Promise.resolve('data'))).toBeUndefined() - }) - - it('should return the data of a thenable that resolves synchronously', () => { - const thenable = { - then: (onFulfilled: (value: string) => unknown) => { - onFulfilled('data') - return Promise.resolve('data') - }, - } as unknown as Promise - - expect(tryResolveSync(thenable)).toEqual({ data: 'data' }) - }) - - it('should support a synchronous thenable whose then() has no catch', () => { - // a React thenable is not always a full promise, so `then` may return - // something without a `catch` method - const thenable = { - then: (onFulfilled: (value: string) => unknown) => { - onFulfilled('data') - }, - } as unknown as Promise - - expect(tryResolveSync(thenable)).toEqual({ data: 'data' }) - }) - - it('should return undefined when a synchronous thenable resolves with undefined', () => { - const thenable = { - then: (onFulfilled: (value: undefined) => unknown) => { - onFulfilled(undefined) - }, - } as unknown as Promise - - expect(tryResolveSync(thenable)).toBeUndefined() - }) - - it('should return undefined for a rejected promise without leaving it unhandled', async () => { - const onUnhandledRejection = vi.fn() - process.on('unhandledRejection', onUnhandledRejection) - - expect(tryResolveSync(Promise.reject(new Error('error')))).toBeUndefined() - await new Promise((resolve) => setTimeout(resolve, 0)) - - process.off('unhandledRejection', onUnhandledRejection) - expect(onUnhandledRejection).not.toHaveBeenCalled() - }) -}) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 976b5faafee..239bef59b65 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -1,4 +1,3 @@ -import { tryResolveSync } from './thenable' import { noop } from './utils' import type { DefaultError, @@ -20,6 +19,25 @@ function defaultTransformerFn(data: any): any { return data } +function tryResolveSync(promise: PromiseLike) { + let data: unknown + + const thenResult = promise.then((result) => { + data = result + return result + }, noop) as Promise | undefined + + // .catch can be unavailable on certain kinds of thenable's + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + thenResult?.catch?.(noop) + + if (data !== undefined) { + return { data } + } + + return undefined +} + export interface DehydrateOptions { serializeData?: TransformerFn shouldDehydrateMutation?: (mutation: Mutation) => boolean diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index dff574f0abd..36ab6e9efe4 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -3,7 +3,6 @@ import { environmentManager } from './environmentManager' import { notifyManager } from './notifyManager' import { fetchState } from './query' import { Subscribable } from './subscribable' -import { pendingThenable } from './thenable' import { isValidTimeout, noop, @@ -17,7 +16,6 @@ import { timeoutManager } from './timeoutManager' import type { ManagedTimerId } from './timeoutManager' import type { FetchOptions, Query, QueryState } from './query' import type { QueryClient } from './queryClient' -import type { PendingThenable, Thenable } from './thenable' import type { DefaultError, DefaultedQueryObserverOptions, @@ -56,7 +54,6 @@ export class QueryObserver< TQueryData, TQueryKey > - #currentThenable: Thenable #selectError: TError | null #selectFn?: (data: TQueryData) => TData #selectResult?: TData @@ -82,7 +79,6 @@ export class QueryObserver< this.#client = client this.#selectError = null - this.#currentThenable = pendingThenable() this.bindMethods() this.setOptions(options) @@ -268,19 +264,6 @@ export class QueryObserver< get: (target, key) => { this.trackProp(key as keyof QueryObserverResult) onPropTracked?.(key as keyof QueryObserverResult) - if (key === 'promise') { - this.trackProp('data') - if ( - !this.options.experimental_prefetchInRender && - this.#currentThenable.status === 'pending' - ) { - this.#currentThenable.reject( - new Error( - 'experimental_prefetchInRender feature flag is not enabled', - ), - ) - } - } return Reflect.get(target, key) }, }) @@ -628,57 +611,11 @@ export class QueryObserver< isRefetchError: isError && hasData, isStale: isStale(query, options), refetch: this.refetch, - promise: this.#currentThenable, isEnabled: resolveQueryBoolean(options.enabled, query) !== false, } const nextResult = result as QueryObserverResult - if (this.options.experimental_prefetchInRender) { - const hasResultData = nextResult.data !== undefined - const isErrorWithoutData = nextResult.status === 'error' && !hasResultData - const finalizeThenableIfPossible = (thenable: PendingThenable) => { - if (isErrorWithoutData) { - thenable.reject(nextResult.error) - } else if (hasResultData) { - thenable.resolve(nextResult.data as TData) - } - } - - /** - * Create a new thenable and result promise when the results have changed - */ - const recreateThenable = () => { - const pending = - (this.#currentThenable = - nextResult.promise = - pendingThenable()) - - finalizeThenableIfPossible(pending) - } - - const prevThenable = this.#currentThenable - switch (prevThenable.status) { - case 'pending': - // Finalize the previous thenable if it was pending - // and we are still observing the same query - if (query.queryHash === prevQuery.queryHash) { - finalizeThenableIfPossible(prevThenable) - } - break - case 'fulfilled': - if (isErrorWithoutData || nextResult.data !== prevThenable.value) { - recreateThenable() - } - break - case 'rejected': - if (!isErrorWithoutData || nextResult.error !== prevThenable.reason) { - recreateThenable() - } - break - } - } - return nextResult } diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts index 0b1bb1833a1..ac1d1a340f0 100644 --- a/packages/query-core/src/retryer.ts +++ b/packages/query-core/src/retryer.ts @@ -1,9 +1,7 @@ import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' -import { pendingThenable } from './thenable' import { environmentManager } from './environmentManager' -import { sleep } from './utils' -import type { Thenable } from './thenable' +import { noop, sleep } from './utils' import type { CancelOptions, DefaultError, NetworkMode } from './types' // TYPES @@ -32,6 +30,8 @@ export interface Retryer { status: () => 'pending' | 'resolved' | 'rejected' } +type RetryerStatus = 'pending' | 'resolved' | 'rejected' + export type RetryValue = boolean | number | ShouldRetryFunction type ShouldRetryFunction = ( @@ -79,11 +79,17 @@ export function createRetryer( let isRetryCancelled = false let failureCount = 0 let continueFn: ((value?: unknown) => void) | undefined + let status: RetryerStatus = 'pending' + let promiseResolve: (data: TData) => void + let promiseReject: (error: TError) => void - const thenable = pendingThenable() + const promise = new Promise((resolve, reject) => { + promiseResolve = resolve + promiseReject = reject + }) + promise.catch(noop) - const isResolved = () => - (thenable.status as Thenable['status']) !== 'pending' + const isResolved = () => status !== 'pending' const cancel = (cancelOptions?: CancelOptions): void => { if (!isResolved()) { @@ -111,14 +117,16 @@ export function createRetryer( const resolve = (value: any) => { if (!isResolved()) { continueFn?.() - thenable.resolve(value) + status = 'resolved' + promiseResolve(value) } } const reject = (value: any) => { if (!isResolved()) { continueFn?.() - thenable.reject(value) + status = 'rejected' + promiseReject(value) } } @@ -206,12 +214,12 @@ export function createRetryer( } return { - promise: thenable, - status: () => thenable.status, + promise, + status: () => status, cancel, continue: () => { continueFn?.() - return thenable + return promise }, cancelRetry, continueRetry, @@ -223,7 +231,7 @@ export function createRetryer( } else { pause().then(run) } - return thenable + return promise }, } } diff --git a/packages/query-core/src/thenable.ts b/packages/query-core/src/thenable.ts deleted file mode 100644 index 034666d079f..00000000000 --- a/packages/query-core/src/thenable.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Thenable types which matches React's types for promises - * - * React seemingly uses `.status`, `.value` and `.reason` properties on a promises to optimistically unwrap data from promises - * - * @see https://github.com/facebook/react/blob/main/packages/shared/ReactTypes.js#L112-L138 - * @see https://github.com/facebook/react/blob/4f604941569d2e8947ce1460a0b2997e835f37b9/packages/react-debug-tools/src/ReactDebugHooks.js#L224-L227 - */ - -import { noop } from './utils' - -interface Fulfilled { - status: 'fulfilled' - value: T -} -interface Rejected { - status: 'rejected' - reason: unknown -} -interface Pending { - status: 'pending' - - /** - * Resolve the promise with a value. - * Will remove the `resolve` and `reject` properties from the promise. - */ - resolve: (value: T) => void - /** - * Reject the promise with a reason. - * Will remove the `resolve` and `reject` properties from the promise. - */ - reject: (reason: unknown) => void -} - -export type FulfilledThenable = Promise & Fulfilled -export type RejectedThenable = Promise & Rejected -export type PendingThenable = Promise & Pending - -export type Thenable = - | FulfilledThenable - | RejectedThenable - | PendingThenable - -export function pendingThenable(): PendingThenable { - let resolve: Pending['resolve'] - let reject: Pending['reject'] - // this could use `Promise.withResolvers()` in the future - const thenable = new Promise((_resolve, _reject) => { - resolve = _resolve - reject = _reject - }) as PendingThenable - - thenable.status = 'pending' - thenable.catch(() => { - // prevent unhandled rejection errors - }) - - function finalize(data: Fulfilled | Rejected) { - if ((thenable as Thenable).status !== 'pending') { - // a caller that kept a reference to `resolve`/`reject` can still invoke it - // after the promise settled, which the underlying promise ignores. Applying - // it here would leave the status disagreeing with the settled value. - return - } - - Object.assign(thenable, data) - - // clear pending props to avoid calling them twice - delete (thenable as Partial>).resolve - delete (thenable as Partial>).reject - } - - thenable.resolve = (value) => { - finalize({ - status: 'fulfilled', - value, - }) - - resolve(value) - } - thenable.reject = (reason) => { - finalize({ - status: 'rejected', - reason, - }) - - reject(reason) - } - - return thenable -} - -/** - * This function takes a Promise-like input and detects whether the data - * is synchronously available or not. - * - * It does not inspect .status, .value or .reason properties of the promise, - * as those are not always available, and the .status of React's promises - * should not be considered part of the public API. - */ -export function tryResolveSync(promise: Promise | Thenable) { - let data: unknown - - promise - .then((result) => { - data = result - return result - }, noop) - // .catch can be unavailable on certain kinds of thenable's - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ?.catch(noop) - - if (data !== undefined) { - return { data } - } - - return undefined -} diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index b71039d332b..34d6a4796ef 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -429,11 +429,6 @@ export interface QueryObserverOptions< > _optimisticResults?: 'optimistic' | 'isRestoring' - - /** - * Enable prefetching during rendering - */ - experimental_prefetchInRender?: boolean } export type WithRequired = TTarget & { @@ -787,55 +782,6 @@ export interface QueryObserverBaseResult< * - See [Network Mode](https://tanstack.com/query/latest/docs/framework/react/guides/network-mode) for more information. */ fetchStatus: FetchStatus - /** - * A stable promise that will be resolved with the data of the query. - * Requires the `experimental_prefetchInRender` feature flag to be enabled. - * @example - * - * ### Enabling the feature flag - * ```ts - * const client = new QueryClient({ - * defaultOptions: { - * queries: { - * experimental_prefetchInRender: true, - * }, - * }, - * }) - * ``` - * - * ### Usage - * ```tsx - * import { useQuery } from '@tanstack/react-query' - * import React from 'react' - * import { fetchTodos, type Todo } from './api' - * - * function TodoList({ query }: { query: UseQueryResult }) { - * const data = React.use(query.promise) - * - * return ( - *
    - * {data.map(todo => ( - *
  • {todo.title}
  • - * ))} - *
- * ) - * } - * - * export function App() { - * const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) - * - * return ( - * <> - *

Todos

- * Loading...}> - * - * - * - * ) - * } - * ``` - */ - promise: Promise } export interface QueryObserverPendingResult< diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx index fdbd401028c..a40705d189b 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx @@ -1,10 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render } from '@testing-library/react' import * as React from 'react' -import { - createRenderStream, - useTrackRenders, -} from '@testing-library/react-render-stream' +import { createRenderStream } from '@testing-library/react-render-stream' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, @@ -39,11 +36,6 @@ describe('useInfiniteQuery', () => { queryCache = new QueryCache() queryClient = new QueryClient({ queryCache, - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, }) }) @@ -106,7 +98,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, @@ -142,7 +133,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) @@ -1777,89 +1767,4 @@ describe('useInfiniteQuery', () => { await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data: custom client')).toBeInTheDocument() }) - - it('should work with React.use()', async () => { - vi.useRealTimers() - - const key = queryKey() - - const renderStream = createRenderStream({ snapshotDOM: true }) - - function Loading() { - useTrackRenders() - return <>loading... - } - - function MyComponent() { - useTrackRenders() - const fetchCountRef = React.useRef(0) - const query = useInfiniteQuery({ - queryFn: ({ pageParam }): Promise => - sleep(10).then(() => ({ - items: [...new Array(10)] - .fill(null) - .map((_, d) => pageParam * pageSize + d), - nextId: pageParam + 1, - prevId: pageParam - 1, - ts: fetchCountRef.current++, - })), - getNextPageParam: (lastPage) => lastPage.nextId, - initialPageParam: 0, - queryKey: key, - }) - const data = React.use(query.promise) - return ( - <> - {data.pages.map((page, index) => ( - -
-
Page: {index + 1}
-
- {page.items.map((item) => ( -

Item: {item}

- ))} -
- ))} - - - ) - } - - function Page() { - useTrackRenders() - return ( - }> - - - ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading...') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('Page: 1') - withinDOM().getByText('Item: 1') - expect(renderedComponents).toEqual([MyComponent]) - } - - // click button - rendered.getByRole('button', { name: 'fetchNextPage' }).click() - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('Page: 1') - expect(renderedComponents).toEqual([MyComponent]) - } - }) }) diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx deleted file mode 100644 index e93c7df5646..00000000000 --- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx +++ /dev/null @@ -1,1453 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import * as React from 'react' -import { ErrorBoundary } from 'react-error-boundary' -import { - createRenderStream, - useTrackRenders, -} from '@testing-library/react-render-stream' -import { queryKey, sleep } from '@tanstack/query-test-utils' -import { - QueryClient, - QueryClientProvider, - QueryErrorResetBoundary, - keepPreviousData, - useInfiniteQuery, - useQuery, -} from '..' -import { QueryCache } from '../index' - -describe('useQuery().promise', { timeout: 10_000 }, () => { - let queryCache: QueryCache - let queryClient: QueryClient - - beforeEach(() => { - vi.useFakeTimers({ - shouldAdvanceTime: true, - toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], - }) - queryCache = new QueryCache() - queryClient = new QueryClient({ - queryCache, - defaultOptions: { - queries: { experimental_prefetchInRender: true }, - }, - }) - }) - - afterEach(() => { - queryClient.clear() - vi.useRealTimers() - }) - - it('should work with a basic test', async () => { - const key = queryKey() - - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - useTrackRenders() - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - - function Page() { - useTrackRenders() - const query = useQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => 'test'), - }) - - return ( - }> -
- -
-
status:{query.status}
-
- ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('colocate suspense and promise', async () => { - const key = queryKey() - let callCount = 0 - - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent() { - useTrackRenders() - const query = useQuery({ - queryKey: key, - queryFn: async () => { - callCount++ - await sleep(10) - return 'test' - }, - staleTime: 1000, - }) - const data = React.use(query.promise) - - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - function Page() { - useTrackRenders() - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - expect(renderedComponents).toEqual([MyComponent]) - } - - expect(callCount).toBe(1) - }) - - it('parallel queries', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - let callCount = 0 - - function MyComponent() { - useTrackRenders() - const query = useQuery({ - queryKey: key, - queryFn: async () => { - callCount++ - await sleep(10) - return 'test' - }, - staleTime: 1000, - }) - const data = React.use(query.promise) - - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - function Page() { - useTrackRenders() - return ( - <> - }> - - - - - - - - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('testtesttesttesttest') - expect(renderedComponents).toEqual([ - MyComponent, - MyComponent, - MyComponent, - MyComponent, - MyComponent, - ]) - } - - expect(callCount).toBe(1) - }) - - it('should work with initial data', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - function Loading() { - useTrackRenders() - - return <>loading.. - } - function Page() { - useTrackRenders() - const query = useQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => 'test'), - initialData: 'initial', - }) - - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('initial') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should not fetch with initial data and staleTime', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'test')) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - function Loading() { - useTrackRenders() - return <>loading.. - } - function Page() { - useTrackRenders() - const query = useQuery({ - queryKey: key, - queryFn, - initialData: 'initial', - staleTime: 1000, - }) - - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('initial') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - // should not call queryFn because of staleTime + initialData combo - expect(queryFn).toHaveBeenCalledTimes(0) - }) - - it('should work with static placeholderData', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - function Loading() { - useTrackRenders() - - return <>loading.. - } - function Page() { - const query = useQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => 'test'), - placeholderData: 'placeholder', - }) - useTrackRenders() - - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('placeholder') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should work with placeholderData: keepPreviousData', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - function Loading() { - useTrackRenders() - - return <>loading.. - } - function Page() { - useTrackRenders() - const [count, setCount] = React.useState(0) - const query = useQuery({ - queryKey: [...key, count], - queryFn: () => sleep(10).then(() => 'test-' + count), - placeholderData: keepPreviousData, - }) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test-0') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - rendered.getByRole('button', { name: 'increment' }).click() - - // re-render because of the increment - { - const { renderedComponents } = await renderStream.takeRender() - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - // re-render with new data, no loading between - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test-1') - // no more suspense boundary rendering - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should be possible to select a part of the data with select', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - - function Page() { - const query = useQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => ({ name: 'test' })), - select: (data) => data.name, - }) - - useTrackRenders() - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should throw error if the promise fails', async () => { - const renderStream = createRenderStream({ snapshotDOM: true }) - const consoleMock = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined) - - const key = queryKey() - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - - let queryCount = 0 - function Page() { - const query = useQuery({ - queryKey: key, - queryFn: () => - sleep(10).then(() => { - if (++queryCount > 1) { - // second time this query mounts, it should not throw - return 'data' - } - throw new Error('Error test') - }), - retry: false, - }) - - return ( - }> - - - ) - } - - const rendered = await renderStream.render( - - - {({ reset }) => ( - ( -
-
error boundary
- -
- )} - > - -
- )} -
-
, - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('error boundary') - } - - consoleMock.mockRestore() - - rendered.getByText('resetErrorBoundary').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('data') - } - - expect(queryCount).toBe(2) - }) - - it('should throw error if the promise fails (colocate suspense and promise)', async () => { - const renderStream = createRenderStream({ snapshotDOM: true }) - const consoleMock = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined) - - const key = queryKey() - - function MyComponent() { - const query = useQuery({ - queryKey: key, - queryFn: () => - sleep(10).then(() => Promise.reject(new Error('Error test'))), - retry: false, - }) - const data = React.use(query.promise) - - return <>{data} - } - - function Page() { - return ( - - - - ) - } - - await renderStream.render( - -
error boundary
}> - -
-
, - ) - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('loading..')).toBeInTheDocument() - } - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('error boundary')).toBeInTheDocument() - } - - consoleMock.mockRestore() - }) - - it('should recreate promise with data changes', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - function Page() { - const query = useQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => 'test1'), - }) - - useTrackRenders() - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test1') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - queryClient.setQueryData(key, 'test2') - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test2') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'test')) - - const options = { - queryKey: key, - queryFn, - } - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const query = useQuery(options) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - rendered.getByText('fetch').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test') - } - - expect(queryFn).toHaveBeenCalledOnce() - }) - - it('should dedupe when re-fetched with refetchQueries while suspending', async () => { - const key = queryKey() - let count = 0 - const renderStream = createRenderStream({ snapshotDOM: true }) - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'test' + count++)) - - const options = { - queryKey: key, - queryFn, - } - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const query = useQuery(options) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - rendered.getByText('refetch').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test0') - } - - expect(queryFn).toHaveBeenCalledOnce() - }) - - it('should stay pending when canceled with cancelQueries while suspending until refetched', async () => { - const renderStream = createRenderStream({ snapshotDOM: true }) - const key = queryKey() - // `sleep` is longer than usual on purpose: with `shouldAdvanceTime`, the - // real time spent rendering and awaiting `takeRender` (~40ms) is added to - // the fake clock, so a shorter fetch would resolve before `cancel` can take - // effect. A longer fetch keeps the query in-flight when it is cancelled. - const queryFn = vi - .fn() - .mockImplementation(() => sleep(50).then(() => 'test')) - - const options = { - queryKey: key, - queryFn, - } - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const query = useQuery(options) - - return ( -
- }> - - - - -
- ) - } - - const rendered = await renderStream.render( - - <>error boundary}> - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - rendered.getByText('cancel').click() - - await vi.waitFor(() => { - const state = queryClient.getQueryState(key) - expect(state).toMatchObject({ - status: 'pending', - fetchStatus: 'idle', - }) - }) - - expect(queryFn).toHaveBeenCalledOnce() - - rendered.getByText('fetch').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('hello') - } - }) - - it('should resolve to previous data when canceled with cancelQueries while suspending', async () => { - const renderStream = createRenderStream({ snapshotDOM: true }) - const key = queryKey() - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'test')) - - const options = { - queryKey: key, - queryFn, - } - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const query = useQuery(options) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - queryClient.setQueryData(key, 'initial') - - rendered.getByText('cancel').click() - - await vi.waitFor(() => { - const state = queryClient.getQueryState(key) - expect(state?.data).toBe('initial') - }) - - expect(queryFn).toHaveBeenCalledTimes(1) - }) - - it('should suspend when not enabled', async () => { - const renderStream = createRenderStream({ snapshotDOM: true }) - const key = queryKey() - - const options = (count: number) => ({ - queryKey: [...key, count], - queryFn: () => sleep(10).then(() => 'test' + count), - }) - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const [count, setCount] = React.useState(0) - const query = useQuery({ ...options(count), enabled: count > 0 }) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('loading..')).toBeInTheDocument() - } - - rendered.getByText('enable').click() - - // loading re-render with enabled - await renderStream.takeRender() - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('test1')).toBeInTheDocument() - } - }) - - it('should show correct data when read from cache only (staleTime)', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - queryClient.setQueryData(key, 'initial') - - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'test')) - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const query = useQuery({ - queryKey: key, - queryFn, - staleTime: Infinity, - }) - - return ( - }> - - - ) - } - - await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('initial') - } - - expect(queryFn).toHaveBeenCalledTimes(0) - }) - - it('should show correct data when switching between cache entries without re-fetches', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent(props: { promise: Promise }) { - useTrackRenders() - const data = React.use(props.promise) - - return <>{data} - } - - function Loading() { - useTrackRenders() - return <>loading.. - } - function Page() { - useTrackRenders() - const [count, setCount] = React.useState(0) - const query = useQuery({ - queryKey: [key, count], - queryFn: () => sleep(10).then(() => 'test' + count), - staleTime: Infinity, - }) - - return ( -
- }> - - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test0') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - rendered.getByText('inc').click() - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(renderedComponents).toEqual([Page, Loading]) - } - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test1') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - - rendered.getByText('dec').click() - - { - const { renderedComponents, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test0') - expect(renderedComponents).toEqual([Page, MyComponent]) - } - }) - - it('should not resolve with intermediate data when keys are switched', async () => { - const key = queryKey() - const renderStream = createRenderStream<{ data: string }>({ - snapshotDOM: true, - }) - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - renderStream.replaceSnapshot({ data }) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const [count, setCount] = React.useState(0) - const query = useQuery({ - queryKey: [key, count], - queryFn: () => sleep(10).then(() => 'test' + count), - staleTime: Infinity, - }) - - return ( -
- }> - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { snapshot, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test0') - expect(snapshot).toMatchObject({ data: 'test0' }) - } - - rendered.getByText('inc').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - rendered.getByText('inc').click() - await renderStream.takeRender() - - rendered.getByText('inc').click() - await renderStream.takeRender() - - { - const { snapshot, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test3') - expect(snapshot).toMatchObject({ data: 'test3' }) - } - }) - - it('should not resolve with intermediate data when keys are switched (with background updates)', async () => { - const key = queryKey() - const renderStream = createRenderStream<{ data: string }>({ - snapshotDOM: true, - }) - let modifier = '' - - function MyComponent(props: { promise: Promise }) { - const data = React.use(props.promise) - - renderStream.replaceSnapshot({ data }) - - return <>{data} - } - - function Loading() { - return <>loading.. - } - function Page() { - const [count, setCount] = React.useState(0) - const query = useQuery({ - queryKey: [key, count], - queryFn: () => sleep(10).then(() => 'test' + count + modifier), - }) - - return ( -
- }> - - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { snapshot, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test0') - expect(snapshot).toMatchObject({ data: 'test0' }) - } - - rendered.getByText('inc').click() - { - const { snapshot } = await renderStream.takeRender() - expect(snapshot).toMatchObject({ data: 'test0' }) - } - - rendered.getByText('inc').click() - { - const { snapshot } = await renderStream.takeRender() - expect(snapshot).toMatchObject({ data: 'test0' }) - } - - rendered.getByText('inc').click() - - { - const { snapshot, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - expect(snapshot).toMatchObject({ data: 'test0' }) - } - - { - const { snapshot, withinDOM } = await renderStream.takeRender() - withinDOM().getByText('test3') - expect(snapshot).toMatchObject({ data: 'test3' }) - } - - modifier = 'new' - - rendered.getByText('dec').click() - await vi.advanceTimersByTimeAsync(11) - expect(rendered.getByText('test2new')).toBeInTheDocument() - - rendered.getByText('dec').click() - await vi.advanceTimersByTimeAsync(11) - expect(rendered.getByText('test1new')).toBeInTheDocument() - - rendered.getByText('dec').click() - await vi.advanceTimersByTimeAsync(11) - expect(rendered.getByText('test0new')).toBeInTheDocument() - }) - - it('should not suspend indefinitely with multiple, nested observers)', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function MyComponent({ input }: { input: string }) { - const query = useTheQuery(input) - const data = React.use(query.promise) - - return <>{data} - } - - function useTheQuery(input: string) { - return useQuery({ - staleTime: Infinity, - queryKey: [key, input], - queryFn: () => sleep(10).then(() => input + ' response'), - }) - } - - function Page() { - const [input, setInput] = React.useState('defaultInput') - useTheQuery(input) - - return ( -
- - - - -
- ) - } - - const rendered = await renderStream.render( - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('defaultInput response') - } - - expect( - queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! - .observers.length, - ).toBe(2) - - rendered.getByText('setInput').click() - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('loading..') - } - - { - const { withinDOM } = await renderStream.takeRender() - withinDOM().getByText('someInput response') - } - - expect( - queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })! - .observers.length, - ).toBe(0) - - expect( - queryClient.getQueryCache().find({ queryKey: [key, 'someInput'] })! - .observers.length, - ).toBe(2) - }) - - it('should implicitly observe data when promise is used', async () => { - const key = queryKey() - - const renderStream = createRenderStream({ snapshotDOM: true }) - - function Page() { - useTrackRenders() - const query = useInfiniteQuery({ - queryKey: key, - queryFn: () => sleep(10).then(() => ({ nextCursor: 1, data: 'test' })), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }) - - React.use(query.promise) - - const hasNextPage = query.hasNextPage - - return ( -
-
hasNextPage: {String(hasNextPage)}
-
- ) - } - - await renderStream.render( - - - - - , - ) - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('loading..')).toBeInTheDocument() - } - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('hasNextPage: true')).toBeInTheDocument() - } - }) - - it('should not throw to error boundary for refetch errors in infinite queries', async () => { - const key = queryKey() - const renderStream = createRenderStream({ snapshotDOM: true }) - - function Page() { - const query = useInfiniteQuery({ - queryKey: key, - queryFn: ({ pageParam = 0 }) => - sleep(10).then(() => { - if (pageParam === 0) { - return { nextCursor: 1, data: 'page-1' } - } - throw new Error('page error') - }), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage.nextCursor, - retry: false, - }) - - const data = React.use(query.promise) - - return ( -
-
pages:{data.pages.length}
-
isError:{String(query.isError)}
-
isFetchNextPageError:{String(query.isFetchNextPageError)}
- -
- ) - } - - const rendered = await renderStream.render( - -
error boundary
}> - - - -
-
, - ) - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('loading..')).toBeInTheDocument() - } - - { - const { withinDOM } = await renderStream.takeRender() - expect(withinDOM().getByText('pages:1')).toBeInTheDocument() - expect(withinDOM().getByText('isError:false')).toBeInTheDocument() - expect( - withinDOM().getByText('isFetchNextPageError:false'), - ).toBeInTheDocument() - } - - rendered.getByText('fetchNext').click() - await vi.advanceTimersByTimeAsync(11) - - expect(rendered.getByText('isFetchNextPageError:true')).toBeInTheDocument() - - expect(rendered.queryByText('error boundary')).toBeNull() - }) -}) diff --git a/packages/react-query/src/__tests__/useQuery.test-d.tsx b/packages/react-query/src/__tests__/useQuery.test-d.tsx index fa104f5f922..b72347821d8 100644 --- a/packages/react-query/src/__tests__/useQuery.test-d.tsx +++ b/packages/react-query/src/__tests__/useQuery.test-d.tsx @@ -16,7 +16,7 @@ describe('useQuery', () => { const fromQueryFn = useQuery({ queryKey: key, queryFn: () => 'test' }) expectTypeOf(fromQueryFn.data).toEqualTypeOf() expectTypeOf(fromQueryFn.error).toEqualTypeOf() - expectTypeOf(fromQueryFn.promise).toEqualTypeOf>() + expectTypeOf(fromQueryFn).not.toHaveProperty('promise') // it should be possible to specify the result type const withResult = useQuery({ diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 05984e66ce5..702d9a3ca4e 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -124,7 +124,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -153,10 +152,7 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) - - expect(states[0]!.promise).toEqual(states[1]!.promise) }) it('should return the correct states for an unsuccessful query', async () => { @@ -216,7 +212,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -245,7 +240,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[2]).toEqual({ @@ -274,11 +268,7 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', - promise: expect.any(Promise), }) - - expect(states[0]!.promise).toEqual(states[1]!.promise) - expect(states[1]!.promise).toEqual(states[2]!.promise) }) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { diff --git a/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx b/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx index da3f47dc540..3c33abd9255 100644 --- a/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx +++ b/packages/react-query/src/__tests__/useSuspenseQuery.test.tsx @@ -1055,11 +1055,6 @@ describe('useSuspenseQuery', () => { const streamedClient = new QueryClient({ queryCache, - defaultOptions: { - queries: { - experimental_prefetchInRender: true, - }, - }, }) const rendered = renderWithClient( diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 734cc74d3de..a27036f9a2c 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -32,11 +32,7 @@ export const ensurePreventErrorBoundaryRetry = < ? shouldThrowError(options.throwOnError, [query.state.error, query]) : options.throwOnError - if ( - options.suspense || - options.experimental_prefetchInRender || - throwOnError - ) { + if (options.suspense || throwOnError) { // Prevent retrying failed query if the error boundary has not been reset yet if (!errorResetBoundary.isReset()) { options.retryOnMount = false diff --git a/packages/react-query/src/suspense.ts b/packages/react-query/src/suspense.ts index d87569f103f..b49e242ef55 100644 --- a/packages/react-query/src/suspense.ts +++ b/packages/react-query/src/suspense.ts @@ -46,11 +46,6 @@ export const ensureSuspenseTimers = ( } } -export const willFetch = ( - result: QueryObserverResult, - isRestoring: boolean, -) => result.isLoading && result.isFetching && !isRestoring - export const shouldSuspend = ( defaultedOptions: | DefaultedQueryObserverOptions diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index e4c446e50e8..43663ea5599 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -163,7 +163,7 @@ export type UseSuspenseQueryResult< TError = DefaultError, > = DistributiveOmit< DefinedQueryObserverResult, - 'isPlaceholderData' | 'promise' + 'isPlaceholderData' > export type DefinedUseQueryResult< @@ -186,7 +186,7 @@ export type UseSuspenseInfiniteQueryResult< TError = DefaultError, > = OmitKeyof< DefinedInfiniteQueryObserverResult, - 'isPlaceholderData' | 'promise' + 'isPlaceholderData' > export type AnyUseMutationOptions = UseMutationOptions diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index eb6a40d9a23..1131fff5dc9 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { environmentManager, noop, notifyManager } from '@tanstack/query-core' +import { noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary' import { @@ -14,7 +14,6 @@ import { ensureSuspenseTimers, fetchOptimistic, shouldSuspend, - willFetch, } from './suspense' import type { QueryClient, @@ -87,11 +86,6 @@ export function useBaseQuery< ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query) useClearResetErrorBoundary(errorResetBoundary) - // this needs to be invoked before creating the Observer because that can create a cache entry - const isNewCacheEntry = !client - .getQueryCache() - .get(defaultedOptions.queryHash) - const [observer] = React.useState( () => new Observer( @@ -150,23 +144,6 @@ export function useBaseQuery< result, ) - if ( - defaultedOptions.experimental_prefetchInRender && - !environmentManager.isServer() && - willFetch(result, isRestoring) - ) { - const promise = isNewCacheEntry - ? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted - fetchOptimistic(defaultedOptions, observer, errorResetBoundary) - : // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in - query?.promise - - promise?.catch(noop).finally(() => { - // `.updateResult()` will trigger `.#currentThenable` to finalize - observer.updateResult() - }) - } - // Handle result property usage tracking return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) diff --git a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx index a863cdb3a79..e3b0c0b702e 100644 --- a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx @@ -109,7 +109,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ data: { pages: [0], pageParams: [0] }, @@ -145,7 +144,6 @@ describe('useInfiniteQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx index 9392af47b6a..f1ce24c4256 100644 --- a/packages/solid-query/src/__tests__/useQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test.tsx @@ -150,7 +150,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -179,7 +178,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) @@ -241,7 +239,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -270,7 +267,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[2]).toEqual({ @@ -299,7 +295,6 @@ describe('useQuery', () => { refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 42096176f67..1cc45a420ba 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -133,9 +133,6 @@ export function useBaseQuery< if (isServer) { defaultOptions.retry = false defaultOptions.throwOnError = true - // Enable prefetch during render for SSR - required for createResource to work - // Without this, queries wait for effects which never run on the server - defaultOptions.experimental_prefetchInRender = true } return defaultOptions }) diff --git a/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts b/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts index 46cf3993dc7..04ec348588e 100644 --- a/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts @@ -69,7 +69,6 @@ describe('createInfiniteQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states.value[1]).toEqual({ @@ -106,7 +105,6 @@ describe('createInfiniteQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) }) diff --git a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts index 3168f732962..e8f7ac82fb0 100644 --- a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts @@ -59,8 +59,6 @@ describe('createQuery', () => { expectTypeOf(query.error).toEqualTypeOf() } - const promise1 = query.promise - expect(query).toEqual({ data: undefined, dataUpdatedAt: 0, @@ -87,7 +85,6 @@ describe('createQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) resolve('resolved') await vi.advanceTimersByTimeAsync(0) @@ -117,10 +114,7 @@ describe('createQuery', () => { refetch: expect.any(Function), status: 'success', fetchStatus: 'idle', - promise: expect.any(Promise), }) - - expect(promise1).toBe(query.promise) }), ) @@ -175,7 +169,6 @@ describe('createQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[1]).toEqual({ @@ -204,7 +197,6 @@ describe('createQuery', () => { refetch: expect.any(Function), status: 'pending', fetchStatus: 'fetching', - promise: expect.any(Promise), }) expect(states[2]).toEqual({ @@ -233,7 +225,6 @@ describe('createQuery', () => { refetch: expect.any(Function), status: 'error', fetchStatus: 'idle', - promise: expect.any(Promise), }) }), )