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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-prefetch-error-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/query-core": patch
---

Align experimental_prefetchInRender promise rejection with Suspense behavior by only throwing when no data is available.
18 changes: 7 additions & 11 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,13 @@ export class QueryObserver<
const nextResult = result as QueryObserverResult<TData, TError>

if (this.options.experimental_prefetchInRender) {
const hasResultData = nextResult.data !== undefined
const isErrorWithoutData = nextResult.status === 'error' && !hasResultData
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
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)
}
}

Expand All @@ -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
Expand Down
67 changes: 67 additions & 0 deletions packages/react-query/src/__tests__/useQuery.promise.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<div>pages:{data.pages.length}</div>
<div>isError:{String(query.isError)}</div>
<div>isFetchNextPageError:{String(query.isFetchNextPageError)}</div>
<button onClick={() => query.fetchNextPage()}>fetchNext</button>
</div>
)
}

const rendered = await renderStream.render(
<QueryClientProvider client={queryClient}>
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
<React.Suspense fallback="loading..">
<Page />
</React.Suspense>
</ErrorBoundary>
</QueryClientProvider>,
)

{
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()
})
})