Skip to content

Commit dea1614

Browse files
HyeongSeokuTkDodo
andauthored
fix(query-core): avoid throwing promise errors when data exists (#10025)
* fix(query-core): avoid throwing promise errors when data exists * test(react-query): cover refetch error behavior with promise + infinite query * chore(changeset): add query-core patch for prefetch error handling --------- Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 4e8be3f commit dea1614

File tree

3 files changed

+79
-11
lines changed

3 files changed

+79
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/query-core": patch
3+
---
4+
5+
Align experimental_prefetchInRender promise rejection with Suspense behavior by only throwing when no data is available.

packages/query-core/src/queryObserver.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -592,11 +592,13 @@ export class QueryObserver<
592592
const nextResult = result as QueryObserverResult<TData, TError>
593593

594594
if (this.options.experimental_prefetchInRender) {
595+
const hasResultData = nextResult.data !== undefined
596+
const isErrorWithoutData = nextResult.status === 'error' && !hasResultData
595597
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
596-
if (nextResult.status === 'error') {
598+
if (isErrorWithoutData) {
597599
thenable.reject(nextResult.error)
598-
} else if (nextResult.data !== undefined) {
599-
thenable.resolve(nextResult.data)
600+
} else if (hasResultData) {
601+
thenable.resolve(nextResult.data as TData)
600602
}
601603
}
602604

@@ -622,18 +624,12 @@ export class QueryObserver<
622624
}
623625
break
624626
case 'fulfilled':
625-
if (
626-
nextResult.status === 'error' ||
627-
nextResult.data !== prevThenable.value
628-
) {
627+
if (isErrorWithoutData || nextResult.data !== prevThenable.value) {
629628
recreateThenable()
630629
}
631630
break
632631
case 'rejected':
633-
if (
634-
nextResult.status !== 'error' ||
635-
nextResult.error !== prevThenable.reason
636-
) {
632+
if (!isErrorWithoutData || nextResult.error !== prevThenable.reason) {
637633
recreateThenable()
638634
}
639635
break

packages/react-query/src/__tests__/useQuery.promise.test.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,4 +1428,71 @@ describe('useQuery().promise', () => {
14281428
expect(withinDOM().getByText('hasNextPage: true')).toBeInTheDocument()
14291429
}
14301430
})
1431+
1432+
it('should not throw to error boundary for refetch errors in infinite queries', async () => {
1433+
const key = queryKey()
1434+
const renderStream = createRenderStream({ snapshotDOM: true })
1435+
1436+
function Page() {
1437+
const query = useInfiniteQuery({
1438+
queryKey: key,
1439+
queryFn: async ({ pageParam = 0 }) => {
1440+
await vi.advanceTimersByTimeAsync(1)
1441+
if (pageParam === 0) {
1442+
return { nextCursor: 1, data: 'page-1' }
1443+
}
1444+
throw new Error('page error')
1445+
},
1446+
initialPageParam: 0,
1447+
getNextPageParam: (lastPage) => lastPage.nextCursor,
1448+
retry: false,
1449+
})
1450+
1451+
const data = React.use(query.promise)
1452+
1453+
return (
1454+
<div>
1455+
<div>pages:{data.pages.length}</div>
1456+
<div>isError:{String(query.isError)}</div>
1457+
<div>isFetchNextPageError:{String(query.isFetchNextPageError)}</div>
1458+
<button onClick={() => query.fetchNextPage()}>fetchNext</button>
1459+
</div>
1460+
)
1461+
}
1462+
1463+
const rendered = await renderStream.render(
1464+
<QueryClientProvider client={queryClient}>
1465+
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
1466+
<React.Suspense fallback="loading..">
1467+
<Page />
1468+
</React.Suspense>
1469+
</ErrorBoundary>
1470+
</QueryClientProvider>,
1471+
)
1472+
1473+
{
1474+
const { withinDOM } = await renderStream.takeRender()
1475+
expect(withinDOM().getByText('loading..')).toBeInTheDocument()
1476+
}
1477+
1478+
{
1479+
const { withinDOM } = await renderStream.takeRender()
1480+
expect(withinDOM().getByText('pages:1')).toBeInTheDocument()
1481+
expect(withinDOM().getByText('isError:false')).toBeInTheDocument()
1482+
expect(
1483+
withinDOM().getByText('isFetchNextPageError:false'),
1484+
).toBeInTheDocument()
1485+
}
1486+
1487+
rendered.getByText('fetchNext').click()
1488+
await vi.advanceTimersByTimeAsync(1)
1489+
1490+
await waitFor(() => {
1491+
expect(
1492+
rendered.getByText('isFetchNextPageError:true'),
1493+
).toBeInTheDocument()
1494+
})
1495+
1496+
expect(rendered.queryByText('error boundary')).toBeNull()
1497+
})
14311498
})

0 commit comments

Comments
 (0)