fix(query-core): keep retrying while unfocused when refetchIntervalInBackground is true - #11119
Conversation
…Background is true The retryer's canContinue gate required focusManager.isFocused(), so a failed background refetch paused until the tab regained focus even with refetchIntervalInBackground set, defeating that option. Thread refetchIntervalInBackground into the retryer and allow retries to continue while unfocused when it is true. Default behavior (pause while unfocused) is unchanged. Fixes TanStack#8353
📝 WalkthroughWalkthrough
ChangesBackground retry continuation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant QueryFetch
participant Retryer
participant FocusManager
QueryFetch->>Retryer: Pass refetchIntervalInBackground
Retryer->>FocusManager: Check focus state
FocusManager-->>Retryer: Page is unfocused
Retryer->>Retryer: Continue retry when background refetch is enabled
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/query-core/src/__tests__/query.test.tsx`:
- Around line 108-143: Update the test around queryClient.fetchQuery so
visibilityMock.mockRestore() always runs in a finally block. Await the fetch
promise directly within try, perform the existing result assertion after it
resolves, and preserve the retry behavior and expectations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 898f080c-f566-4902-80ae-41947e0701aa
📒 Files selected for processing (5)
.changeset/quiet-retries-keep-running.mdpackages/query-core/src/__tests__/query.test.tsxpackages/query-core/src/query.tspackages/query-core/src/retryer.tspackages/query-core/src/types.ts
| it('should keep retrying while unfocused when refetchIntervalInBackground is true', async () => { | ||
| const key = queryKey() | ||
|
|
||
| // make page unfocused | ||
| const visibilityMock = mockVisibilityState('hidden') | ||
|
|
||
| let count = 0 | ||
| let result | ||
|
|
||
| const promise = queryClient.fetchQuery({ | ||
| queryKey: key, | ||
| queryFn: () => { | ||
| count++ | ||
|
|
||
| if (count === 3) { | ||
| return `data${count}` | ||
| } | ||
|
|
||
| throw new Error(`error${count}`) | ||
| }, | ||
| retry: 3, | ||
| retryDelay: 1, | ||
| refetchIntervalInBackground: true, | ||
| }) | ||
|
|
||
| promise.then((data) => { | ||
| result = data | ||
| }) | ||
|
|
||
| // Retries proceed even though the page stays unfocused | ||
| await vi.advanceTimersByTimeAsync(50) | ||
| expect(result).toBe('data3') | ||
|
|
||
| visibilityMock.mockRestore() | ||
| }) | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Restore the visibility mock in a finally block.
If the fetch or assertion fails, mockRestore() is skipped and can contaminate subsequent tests in the same worker. Await the promise directly inside try/finally.
Proposed fix
- let result
-
const promise = queryClient.fetchQuery({
...
})
- promise.then((data) => {
- result = data
- })
-
- await vi.advanceTimersByTimeAsync(50)
- expect(result).toBe('data3')
-
- visibilityMock.mockRestore()
+ try {
+ await vi.advanceTimersByTimeAsync(50)
+ await expect(promise).resolves.toBe('data3')
+ } finally {
+ visibilityMock.mockRestore()
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should keep retrying while unfocused when refetchIntervalInBackground is true', async () => { | |
| const key = queryKey() | |
| // make page unfocused | |
| const visibilityMock = mockVisibilityState('hidden') | |
| let count = 0 | |
| let result | |
| const promise = queryClient.fetchQuery({ | |
| queryKey: key, | |
| queryFn: () => { | |
| count++ | |
| if (count === 3) { | |
| return `data${count}` | |
| } | |
| throw new Error(`error${count}`) | |
| }, | |
| retry: 3, | |
| retryDelay: 1, | |
| refetchIntervalInBackground: true, | |
| }) | |
| promise.then((data) => { | |
| result = data | |
| }) | |
| // Retries proceed even though the page stays unfocused | |
| await vi.advanceTimersByTimeAsync(50) | |
| expect(result).toBe('data3') | |
| visibilityMock.mockRestore() | |
| }) | |
| it('should keep retrying while unfocused when refetchIntervalInBackground is true', async () => { | |
| const key = queryKey() | |
| // make page unfocused | |
| const visibilityMock = mockVisibilityState('hidden') | |
| let count = 0 | |
| const promise = queryClient.fetchQuery({ | |
| queryKey: key, | |
| queryFn: () => { | |
| count++ | |
| if (count === 3) { | |
| return `data${count}` | |
| } | |
| throw new Error(`error${count}`) | |
| }, | |
| retry: 3, | |
| retryDelay: 1, | |
| refetchIntervalInBackground: true, | |
| }) | |
| try { | |
| await vi.advanceTimersByTimeAsync(50) | |
| await expect(promise).resolves.toBe('data3') | |
| } finally { | |
| visibilityMock.mockRestore() | |
| } | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/query-core/src/__tests__/query.test.tsx` around lines 108 - 143,
Update the test around queryClient.fetchQuery so visibilityMock.mockRestore()
always runs in a finally block. Await the fetch promise directly within try,
perform the existing result assertion after it resolves, and preserve the retry
behavior and expectations.
Fixes #8353
Problem
With
retryandrefetchIntervalInBackground: true, a failed background refetch does not retry while the tab is inactive — the retryer pauses until the page regains focus, which defeats the purpose ofrefetchIntervalInBackground.Cause
Retryer.canContinuegates continuation onfocusManager.isFocused():So while the tab is unfocused, a paused retry never continues.
Fix
This follows the direction agreed by the maintainers in the earlier discussion on #9563 (checking the
refetchIntervalInBackgroundprop additionally in the retryer, rather than removing the focus check):refetchIntervalInBackgroundinto the retryer config (query.ts→retryer.ts).canContinuenow allows continuation while unfocused whenrefetchIntervalInBackground === true.Default behavior (retries pause while the tab is unfocused) is unchanged when the option is unset.
Testing
Added
should keep retrying while unfocused when refetchIntervalInBackground is truetoquery.test.tsx. It fails onmain(expected undefined to be 'data3') and passes with this change. Full@tanstack/query-coresuite: 555 tests pass, no type errors; prettier and eslint clean. Changeset included (patch).Summary by CodeRabbit
Bug Fixes
Tests