Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3268587
test(react-query): add reproduction test for useQuery().promise retry…
zaewc Jan 25, 2026
fb79e38
fix(query-core): recreate promise when fetchStatus changes to fetching
zaewc Jan 26, 2026
5ee860a
fix(react-query): trigger refetch of errored queries on reset
zaewc Jan 27, 2026
ffd9b1b
fix: Refine QueryErrorResetBoundary's reset logic to only refetch err…
zaewc Jan 28, 2026
ade1696
ci: apply automated fixes
autofix-ci[bot] Jan 29, 2026
2f9a7fe
feat: Export resolveEnabled and use it for observer enabled checks
zaewc Jan 29, 2026
00af302
ci: apply automated fixes
autofix-ci[bot] Jan 29, 2026
15339fe
feat(query-core): attach queryHash to observer promises
zaewc Jan 31, 2026
7c81f75
feat(query-core): attach queryHash to observer promises
zaewc Jan 31, 2026
7c2fd9e
feat(react-query): register query hashes in QueryErrorResetBoundary
zaewc Jan 31, 2026
84e20a7
test(react-query): add core invariant tests for QueryErrorResetBounda…
zaewc Jan 31, 2026
171690d
test(react-query): merge and update scoped registry tests
zaewc Jan 31, 2026
aec13b3
ci: apply automated fixes
autofix-ci[bot] Jan 31, 2026
e1f4248
Update packages/react-query/src/__tests__/QueryResetErrorBoundary.tes…
zaewc Jan 31, 2026
d4cd23a
chore: remove an extraneous brace
zaewc Jan 31, 2026
c2c0823
Merge branch 'main' into fix/query-promise-reset
zaewc Feb 24, 2026
902b55d
Merge branch 'main' into fix/query-promise-reset
zaewc Mar 22, 2026
208d0ed
Merge branch 'main' into fix/query-promise-reset
zaewc Apr 8, 2026
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,
),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
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