From 26772686a8ae7774c282cf3744a70aa4ea8e65df Mon Sep 17 00:00:00 2001 From: Sumit Date: Sun, 11 Oct 2026 10:24:45 +0530 Subject: [PATCH 1/3] fix(error-boundary): avoid clearing reset state without a pending reset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/fix-query-error-reset-boundary.md | 6 ++++++ .../QueryResetErrorBoundary.test.tsx | 19 +++++++++++++++++++ .../preact-query/src/errorBoundaryUtils.ts | 4 +++- .../QueryResetErrorBoundary.test.tsx | 19 +++++++++++++++++++ .../react-query/src/errorBoundaryUtils.ts | 4 +++- 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-query-error-reset-boundary.md diff --git a/.changeset/fix-query-error-reset-boundary.md b/.changeset/fix-query-error-reset-boundary.md new file mode 100644 index 00000000000..db15a74b492 --- /dev/null +++ b/.changeset/fix-query-error-reset-boundary.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-query': patch +'@tanstack/preact-query': patch +--- + +fix(error-boundary): only clear a reset boundary after it has actually been reset diff --git a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx index 43c1e9df144..f6538faf0df 100644 --- a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -13,6 +13,7 @@ import { useSuspenseQueries, useSuspenseQuery, } from '..' +import { useClearResetErrorBoundary } from '../errorBoundaryUtils' import { ErrorBoundary } from './ErrorBoundary' import { renderWithClient } from './utils' @@ -32,6 +33,24 @@ describe('QueryErrorResetBoundary', () => { }) describe('useQuery', () => { + it('should only clear the reset boundary after a reset has happened', () => { + const clearReset = vi.fn() + + function TestComponent() { + useClearResetErrorBoundary({ + clearReset, + isReset: () => false, + reset: vi.fn(), + }) + + return null + } + + renderWithClient(queryClient, ) + + expect(clearReset).not.toHaveBeenCalled() + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/packages/preact-query/src/errorBoundaryUtils.ts b/packages/preact-query/src/errorBoundaryUtils.ts index fecc039f7d3..1ecf0b4ddb0 100644 --- a/packages/preact-query/src/errorBoundaryUtils.ts +++ b/packages/preact-query/src/errorBoundaryUtils.ts @@ -57,7 +57,9 @@ export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { useEffect(() => { - errorResetBoundary.clearReset() + if (errorResetBoundary.isReset()) { + errorResetBoundary.clearReset() + } }, [errorResetBoundary]) } diff --git a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx index 9479cad27b3..edd922a3934 100644 --- a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -12,6 +12,7 @@ import { useSuspenseQueries, useSuspenseQuery, } from '..' +import { useClearResetErrorBoundary } from '../errorBoundaryUtils' import { renderWithClient } from './utils' describe('QueryErrorResetBoundary', () => { @@ -30,6 +31,24 @@ describe('QueryErrorResetBoundary', () => { }) describe('useQuery', () => { + it('should only clear the reset boundary after a reset has happened', () => { + const clearReset = vi.fn() + + function TestComponent() { + useClearResetErrorBoundary({ + clearReset, + isReset: () => false, + reset: vi.fn(), + }) + + return null + } + + renderWithClient(queryClient, ) + + expect(clearReset).not.toHaveBeenCalled() + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 2d6824178a4..3b8cc46d179 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -57,7 +57,9 @@ export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { React.useEffect(() => { - errorResetBoundary.clearReset() + if (errorResetBoundary.isReset()) { + errorResetBoundary.clearReset() + } }, [errorResetBoundary]) } From 299647ced21a450aac0913ecc8cae2ab5dfad3bf Mon Sep 17 00:00:00 2001 From: Sumit Date: Tue, 13 Oct 2026 17:29:09 +0530 Subject: [PATCH 2/3] fix(error-boundary): track reset consumption per boundary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/QueryErrorResetBoundary.tsx | 16 +++++++- .../QueryResetErrorBoundary.test.tsx | 37 +++++++++++++++++++ .../preact-query/src/errorBoundaryUtils.ts | 11 +++++- .../src/QueryErrorResetBoundary.tsx | 23 +++++++++++- .../QueryResetErrorBoundary.test.tsx | 37 +++++++++++++++++++ .../react-query/src/errorBoundaryUtils.ts | 11 +++++- 6 files changed, 131 insertions(+), 4 deletions(-) diff --git a/packages/preact-query/src/QueryErrorResetBoundary.tsx b/packages/preact-query/src/QueryErrorResetBoundary.tsx index 9f5e1045a9a..76bb27c6caf 100644 --- a/packages/preact-query/src/QueryErrorResetBoundary.tsx +++ b/packages/preact-query/src/QueryErrorResetBoundary.tsx @@ -43,18 +43,32 @@ export interface QueryErrorResetBoundaryValue { * requested. * @returns The `clearReset`, `isReset`, and `reset` functions of the boundary. */ -function createValue(): QueryErrorResetBoundaryValue { +function createValue(): QueryErrorResetBoundaryValue & { + _resetCount: () => number + _lastConsumedResetCount: () => number +} { let isReset = false + let resetCount = 0 + let lastConsumedResetCount = 0 + return { clearReset: () => { isReset = false + lastConsumedResetCount = resetCount }, reset: () => { isReset = true + resetCount++ }, isReset: () => { return isReset }, + _resetCount: () => { + return resetCount + }, + _lastConsumedResetCount: () => { + return lastConsumedResetCount + }, } } diff --git a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx index f6538faf0df..f54e39403fa 100644 --- a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -51,6 +51,43 @@ describe('QueryErrorResetBoundary', () => { expect(clearReset).not.toHaveBeenCalled() }) + it('should only consume a reset once per generation', () => { + let isReset = false + let resetCount = 0 + let lastConsumedResetCount = 0 + const boundary = { + clearReset: vi.fn(() => { + isReset = false + lastConsumedResetCount = resetCount + }), + isReset: () => isReset, + reset: () => { + isReset = true + resetCount++ + }, + _resetCount: () => resetCount, + _lastConsumedResetCount: () => lastConsumedResetCount, + } + + boundary.reset() + if ( + boundary._lastConsumedResetCount() !== boundary._resetCount() && + boundary.isReset() + ) { + boundary.clearReset() + } + expect(boundary.clearReset).toHaveBeenCalledTimes(1) + + isReset = true + if ( + boundary._lastConsumedResetCount() !== boundary._resetCount() && + boundary.isReset() + ) { + boundary.clearReset() + } + expect(boundary.clearReset).toHaveBeenCalledTimes(1) + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/packages/preact-query/src/errorBoundaryUtils.ts b/packages/preact-query/src/errorBoundaryUtils.ts index 1ecf0b4ddb0..4cea74fee0e 100644 --- a/packages/preact-query/src/errorBoundaryUtils.ts +++ b/packages/preact-query/src/errorBoundaryUtils.ts @@ -57,7 +57,16 @@ export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { useEffect(() => { - if (errorResetBoundary.isReset()) { + const resetBoundary = errorResetBoundary as QueryErrorResetBoundaryValue & { + _resetCount?: () => number + _lastConsumedResetCount?: () => number + } + + if ( + resetBoundary._lastConsumedResetCount?.() !== + resetBoundary._resetCount?.() && + errorResetBoundary.isReset() + ) { errorResetBoundary.clearReset() } }, [errorResetBoundary]) diff --git a/packages/react-query/src/QueryErrorResetBoundary.tsx b/packages/react-query/src/QueryErrorResetBoundary.tsx index dfde44fb268..4b0e7e186e8 100644 --- a/packages/react-query/src/QueryErrorResetBoundary.tsx +++ b/packages/react-query/src/QueryErrorResetBoundary.tsx @@ -42,20 +42,28 @@ export interface QueryErrorResetBoundaryValue { * requested. * @returns The `clearReset`, `isReset`, and `reset` functions of the boundary. */ -function createValue(): QueryErrorResetBoundaryValue { +function createValue(): QueryErrorResetBoundaryValue & { + _resetCount: () => number + _lastConsumedResetCount: () => number +} { let isReset = false + let resetCount = 0 + let lastConsumedResetCount = 0 + return { /** * Clears the reset state, so queries know not to try again until the boundary is reset again. */ clearReset: () => { isReset = false + lastConsumedResetCount = resetCount }, /** * Resets any query errors within the boundary, so queries know they can try again. */ reset: () => { isReset = true + resetCount++ }, /** * Returns whether the boundary has been reset and not yet cleared. @@ -64,6 +72,19 @@ function createValue(): QueryErrorResetBoundaryValue { isReset: () => { return isReset }, + /** + * Tracks the current reset generation so a remount can consume only the + * most recent pending reset. + */ + _resetCount: () => { + return resetCount + }, + /** + * Tracks the latest reset generation that has already been consumed. + */ + _lastConsumedResetCount: () => { + return lastConsumedResetCount + }, } } diff --git a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx index edd922a3934..329cffade31 100644 --- a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -49,6 +49,43 @@ describe('QueryErrorResetBoundary', () => { expect(clearReset).not.toHaveBeenCalled() }) + it('should only consume a reset once per generation', () => { + let isReset = false + let resetCount = 0 + let lastConsumedResetCount = 0 + const boundary = { + clearReset: vi.fn(() => { + isReset = false + lastConsumedResetCount = resetCount + }), + isReset: () => isReset, + reset: () => { + isReset = true + resetCount++ + }, + _resetCount: () => resetCount, + _lastConsumedResetCount: () => lastConsumedResetCount, + } + + boundary.reset() + if ( + boundary._lastConsumedResetCount() !== boundary._resetCount() && + boundary.isReset() + ) { + boundary.clearReset() + } + expect(boundary.clearReset).toHaveBeenCalledTimes(1) + + isReset = true + if ( + boundary._lastConsumedResetCount() !== boundary._resetCount() && + boundary.isReset() + ) { + boundary.clearReset() + } + expect(boundary.clearReset).toHaveBeenCalledTimes(1) + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 3b8cc46d179..49b08054039 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -57,7 +57,16 @@ export const useClearResetErrorBoundary = ( errorResetBoundary: QueryErrorResetBoundaryValue, ) => { React.useEffect(() => { - if (errorResetBoundary.isReset()) { + const resetBoundary = errorResetBoundary as QueryErrorResetBoundaryValue & { + _resetCount?: () => number + _lastConsumedResetCount?: () => number + } + + if ( + resetBoundary._lastConsumedResetCount?.() !== + resetBoundary._resetCount?.() && + errorResetBoundary.isReset() + ) { errorResetBoundary.clearReset() } }, [errorResetBoundary]) From 82870bce2d0d0579eeffb80616ec7334d36b68bb Mon Sep 17 00:00:00 2001 From: Sumit Date: Wed, 14 Oct 2026 08:27:51 +0530 Subject: [PATCH 3/3] fix(error-boundary): ignore repeated reset clears Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/QueryErrorResetBoundary.tsx | 4 +++ .../QueryResetErrorBoundary.test.tsx | 35 +++++++++++++++++++ .../src/QueryErrorResetBoundary.tsx | 4 +++ .../QueryResetErrorBoundary.test.tsx | 35 +++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/packages/preact-query/src/QueryErrorResetBoundary.tsx b/packages/preact-query/src/QueryErrorResetBoundary.tsx index 76bb27c6caf..aca4d46c07c 100644 --- a/packages/preact-query/src/QueryErrorResetBoundary.tsx +++ b/packages/preact-query/src/QueryErrorResetBoundary.tsx @@ -53,6 +53,10 @@ function createValue(): QueryErrorResetBoundaryValue & { return { clearReset: () => { + if (lastConsumedResetCount === resetCount) { + return + } + isReset = false lastConsumedResetCount = resetCount }, diff --git a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx index f54e39403fa..7c578b7123f 100644 --- a/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/preact-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -57,6 +57,10 @@ describe('QueryErrorResetBoundary', () => { let lastConsumedResetCount = 0 const boundary = { clearReset: vi.fn(() => { + if (lastConsumedResetCount === resetCount) { + return + } + isReset = false lastConsumedResetCount = resetCount }), @@ -88,6 +92,37 @@ describe('QueryErrorResetBoundary', () => { expect(boundary.clearReset).toHaveBeenCalledTimes(1) }) + it('should ignore repeated clearReset calls for the same generation', () => { + let isReset = true + let resetCount = 1 + let lastConsumedResetCount = 0 + + const boundary = { + clearReset: vi.fn(() => { + if (lastConsumedResetCount === resetCount) { + return + } + + isReset = false + lastConsumedResetCount = resetCount + }), + isReset: () => isReset, + reset: () => { + isReset = true + resetCount += 1 + }, + _resetCount: () => resetCount, + _lastConsumedResetCount: () => lastConsumedResetCount, + } + + boundary.clearReset() + expect(isReset).toBe(false) + expect(lastConsumedResetCount).toBe(1) + + boundary.clearReset() + expect(lastConsumedResetCount).toBe(1) + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error') diff --git a/packages/react-query/src/QueryErrorResetBoundary.tsx b/packages/react-query/src/QueryErrorResetBoundary.tsx index 4b0e7e186e8..cd2125ef22d 100644 --- a/packages/react-query/src/QueryErrorResetBoundary.tsx +++ b/packages/react-query/src/QueryErrorResetBoundary.tsx @@ -55,6 +55,10 @@ function createValue(): QueryErrorResetBoundaryValue & { * Clears the reset state, so queries know not to try again until the boundary is reset again. */ clearReset: () => { + if (lastConsumedResetCount === resetCount) { + return + } + isReset = false lastConsumedResetCount = resetCount }, diff --git a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx index 329cffade31..7ef8e332e21 100644 --- a/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx +++ b/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx @@ -55,6 +55,10 @@ describe('QueryErrorResetBoundary', () => { let lastConsumedResetCount = 0 const boundary = { clearReset: vi.fn(() => { + if (lastConsumedResetCount === resetCount) { + return + } + isReset = false lastConsumedResetCount = resetCount }), @@ -86,6 +90,37 @@ describe('QueryErrorResetBoundary', () => { expect(boundary.clearReset).toHaveBeenCalledTimes(1) }) + it('should ignore repeated clearReset calls for the same generation', () => { + let isReset = true + let resetCount = 1 + let lastConsumedResetCount = 0 + + const boundary = { + clearReset: vi.fn(() => { + if (lastConsumedResetCount === resetCount) { + return + } + + isReset = false + lastConsumedResetCount = resetCount + }), + isReset: () => isReset, + reset: () => { + isReset = true + resetCount += 1 + }, + _resetCount: () => resetCount, + _lastConsumedResetCount: () => lastConsumedResetCount, + } + + boundary.clearReset() + expect(isReset).toBe(false) + expect(lastConsumedResetCount).toBe(1) + + boundary.clearReset() + expect(lastConsumedResetCount).toBe(1) + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleErrorMock = vi .spyOn(console, 'error')