Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 5 additions & 1 deletion packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,11 @@ export class QueryObserver<
}
break
case 'rejected':
if (!isErrorWithoutData || nextResult.error !== prevThenable.reason) {
if (
!isErrorWithoutData ||
nextResult.error !== prevThenable.reason ||
nextResult.fetchStatus === 'fetching'
) {
recreateThenable()
}
break
Expand Down
16 changes: 14 additions & 2 deletions packages/react-query/src/QueryErrorResetBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client'
import * as React from 'react'

import { useQueryClient } from './QueryClientProvider'
import type { QueryClient } from '@tanstack/query-core'

// CONTEXT
export type QueryErrorResetFunction = () => void
export type QueryErrorIsResetFunction = () => boolean
Expand All @@ -12,14 +15,22 @@ export interface QueryErrorResetBoundaryValue {
reset: QueryErrorResetFunction
}

function createValue(): QueryErrorResetBoundaryValue {
function createValue(client?: QueryClient): QueryErrorResetBoundaryValue {
let isReset = false
return {
clearReset: () => {
isReset = false
},
reset: () => {
isReset = true
void client?.refetchQueries({
predicate: (query) =>
query.state.status === 'error' &&
query.getObserversCount() > 0 &&
query.observers.some(
(observer) => observer.options.enabled !== false,
),
})
},
isReset: () => {
return isReset
Expand Down Expand Up @@ -47,7 +58,8 @@ export interface QueryErrorResetBoundaryProps {
export const QueryErrorResetBoundary = ({
children,
}: QueryErrorResetBoundaryProps) => {
const [value] = React.useState(() => createValue())
const client = useQueryClient()
const [value] = React.useState(() => createValue(client))
return (
<QueryErrorResetBoundaryContext.Provider value={value}>
{typeof children === 'function' ? children(value) : children}
Expand Down
75 changes: 75 additions & 0 deletions packages/react-query/src/__tests__/useQuery.promise.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1504,4 +1504,79 @@ describe('useQuery().promise', { timeout: 10_000 }, () => {

expect(rendered.queryByText('error boundary')).toBeNull()
})

it('should retry when QERB triggers reset, even if useQuery is outside', async () => {
const key = queryKey()
const renderStream = createRenderStream({ snapshotDOM: true })

let queryCount = 0
function Child(props: { promise: Promise<string> }) {
const data = React.use(props.promise)
return <>{data}</>
}

function Page() {
const query = useQuery({
queryKey: key,
queryFn: async () => {
await vi.advanceTimersByTimeAsync(1)
queryCount++
if (queryCount === 1) {
throw new Error('Error test')
}
return 'data'
},
retry: false,
})

return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
resetKeys={[query.promise]}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button onClick={resetErrorBoundary}>retry</button>
</div>
)}
>
<React.Suspense fallback={<div>loading..</div>}>
<Child promise={query.promise} />
</React.Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}

const rendered = await renderStream.render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)

{
const { withinDOM } = await renderStream.takeRender()
expect(withinDOM().getByText('loading..')).toBeInTheDocument()
}

{
const { withinDOM } = await renderStream.takeRender()
expect(withinDOM().getByText('error boundary')).toBeInTheDocument()
}

rendered.getByText('retry').click()

await waitFor(() => {
expect(rendered.getByText('loading..')).toBeInTheDocument()
})

await waitFor(() => {
expect(rendered.getByText('data')).toBeInTheDocument()
})

expect(queryCount).toBe(2)
})
})
Loading