diff --git a/.changeset/fix-prefetch-error-boundary.md b/.changeset/fix-prefetch-error-boundary.md new file mode 100644 index 0000000000..2e80eb7e38 --- /dev/null +++ b/.changeset/fix-prefetch-error-boundary.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-core": patch +--- + +Align experimental_prefetchInRender promise rejection with Suspense behavior by only throwing when no data is available. diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 92978673f6..c05c25450c 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -592,11 +592,13 @@ export class QueryObserver< 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 (nextResult.status === 'error') { + if (isErrorWithoutData) { thenable.reject(nextResult.error) - } else if (nextResult.data !== undefined) { - thenable.resolve(nextResult.data) + } else if (hasResultData) { + thenable.resolve(nextResult.data as TData) } } @@ -622,18 +624,12 @@ export class QueryObserver< } break case 'fulfilled': - if ( - nextResult.status === 'error' || - nextResult.data !== prevThenable.value - ) { + if (isErrorWithoutData || nextResult.data !== prevThenable.value) { recreateThenable() } break case 'rejected': - if ( - nextResult.status !== 'error' || - nextResult.error !== prevThenable.reason - ) { + if (!isErrorWithoutData || nextResult.error !== prevThenable.reason) { recreateThenable() } break diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx index cef239a276..d376a4970b 100644 --- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.promise.test.tsx @@ -1428,4 +1428,71 @@ describe('useQuery().promise', () => { 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: async ({ pageParam = 0 }) => { + await vi.advanceTimersByTimeAsync(1) + 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(1) + + await waitFor(() => { + expect( + rendered.getByText('isFetchNextPageError:true'), + ).toBeInTheDocument() + }) + + expect(rendered.queryByText('error boundary')).toBeNull() + }) })