Skip to content

fix(query-core): keep retrying while unfocused when refetchIntervalInBackground is true - #11119

Closed
makoto-developer wants to merge 2 commits into
TanStack:mainfrom
makoto-developer:fix-retry-refetch-interval-background
Closed

fix(query-core): keep retrying while unfocused when refetchIntervalInBackground is true#11119
makoto-developer wants to merge 2 commits into
TanStack:mainfrom
makoto-developer:fix-retry-refetch-interval-background

Conversation

@makoto-developer

@makoto-developermakoto-developer commented Jul 26, 2026

Copy link
Copy Markdown

Fixes#8353

Problem

With retry and refetchIntervalInBackground: 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 of refetchIntervalInBackground.

Cause

Retryer.canContinue gates continuation on focusManager.isFocused():

constcanContinue=()=>focusManager.isFocused()&&(config.networkMode==='always'||onlineManager.isOnline())&&config.canRun()

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 refetchIntervalInBackground prop additionally in the retryer, rather than removing the focus check):

  • Thread refetchIntervalInBackground into the retryer config (query.tsretryer.ts).
  • canContinue now allows continuation while unfocused when refetchIntervalInBackground === 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 true to query.test.tsx. It fails on main (expected undefined to be 'data3') and passes with this change. Full @tanstack/query-core suite: 555 tests pass, no type errors; prettier and eslint clean. Changeset included (patch).


  • I have followed the steps in the contributing guide.
  • This change affects published code, and I have generated a changeset.

Summary by CodeRabbit

  • Bug Fixes

    • Failed query retries now continue while the tab is in the background when background refetching is enabled.
    • Existing focus and network conditions continue to be respected when background refetching is disabled.
    • Background refetching settings now consistently apply to retry behavior.
  • Tests

    • Added coverage for retry behavior while the page is unfocused and hidden.

…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.
FixesTanStack#8353
@coderabbitai

coderabbitaiBot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fee6bdd6-e151-4e71-ab3f-6cea0127f6bc

📥 Commits

Reviewing files that changed from the base of the PR and between 1f631b3 and 3140ee5.

📒 Files selected for processing (5)
  • .changeset/quiet-retries-keep-running.md
  • packages/query-core/src/__tests__/query.test.tsx
  • packages/query-core/src/query.ts
  • packages/query-core/src/retryer.ts
  • packages/query-core/src/types.ts

📝 Walkthrough

Walkthrough

refetchIntervalInBackground now reaches the retryer from query options. Failed-query retries continue while the page is unfocused when the option is enabled. Tests and a changeset document the behavior.

Changes

Background retry continuation

Layer / File(s)Summary
Propagate background retry option
packages/query-core/src/types.ts, packages/query-core/src/retryer.ts, packages/query-core/src/query.ts
The option is defined in QueryOptions, added to RetryerConfig, and forwarded by Query.fetch.
Allow and validate background retries
packages/query-core/src/retryer.ts, packages/query-core/src/__tests__/query.test.tsx, .changeset/quiet-retries-keep-running.md
Retry continuation permits unfocused execution when background refetching is enabled. A hidden-page test verifies two failures followed by a successful retry. The changeset records the behavior.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main behavior fix in query-core and matches the implemented retry/background change.
Description check✅ PassedThe description covers the problem, cause, fix, testing, and release impact, though it doesn't use the template headings verbatim.
Linked Issues check✅ PassedThe code changes and new test address #8353 by letting retries continue while unfocused when refetchIntervalInBackground is enabled.
Out of Scope Changes check✅ PassedThe diff stays focused on the retry/background fix, with only supporting type, test, and changeset updates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@makoto-developer
makoto-developer marked this pull request as ready for review July 26, 2026 02:35

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fd50fa1 and 6bba5b4.

📒 Files selected for processing (5)
  • .changeset/quiet-retries-keep-running.md
  • packages/query-core/src/__tests__/query.test.tsx
  • packages/query-core/src/query.ts
  • packages/query-core/src/retryer.ts
  • packages/query-core/src/types.ts

Comment on lines +108 to +143
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()
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
it('should keep retrying while unfocused when refetchIntervalInBackground is true',async()=>{
constkey=queryKey()
// make page unfocused
constvisibilityMock=mockVisibilityState('hidden')
letcount=0
letresult
constpromise=queryClient.fetchQuery({
queryKey: key,
queryFn: ()=>{
count++
if(count===3){
return`data${count}`
}
thrownewError(`error${count}`)
},
retry: 3,
retryDelay: 1,
refetchIntervalInBackground: true,
})
promise.then((data)=>{
result=data
})
// Retries proceed even though the page stays unfocused
awaitvi.advanceTimersByTimeAsync(50)
expect(result).toBe('data3')
visibilityMock.mockRestore()
})
it('should keep retrying while unfocused when refetchIntervalInBackground is true',async()=>{
constkey=queryKey()
// make page unfocused
constvisibilityMock=mockVisibilityState('hidden')
letcount=0
constpromise=queryClient.fetchQuery({
queryKey: key,
queryFn: ()=>{
count++
if(count===3){
return`data${count}`
}
thrownewError(`error${count}`)
},
retry: 3,
retryDelay: 1,
refetchIntervalInBackground: true,
})
try{
awaitvi.advanceTimersByTimeAsync(50)
awaitexpect(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.

@coderabbitai

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@nx-cloud

nx-cloudBot commented Aug 18, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 3140ee5

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded7m 59sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded41sView ↗

☁️ Nx Cloud last updated this comment at 2026-08-18 17:03:54 UTC

@TkDodo

Copy link
Copy Markdown
Collaborator

@TkDodoTkDodo closed this Aug 18, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Polling Stops with refetchIntervalInBackground and Retry When Tab Is Inactive

2 participants

@makoto-developer@TkDodo