Skip to content

fix(query-core): remove focus check from retryer for refetchIntervallnBackground #9563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Hellol77
Copy link

@Hellol77 Hellol77 commented Aug 13, 2025

fixed #8353

Summary

  • Removes focusManager.isFocused() check from retryer's canContinue()
    function
  • Updates tests to verify background retry behavior

test modifications

  • "should continue retry after focus regain" → "should continue retry and resolve even with focus state changes"
    • Removed logic that waited for visibilitychange events
    • Changed expectations from paused behavior to continuous retry behavior
  • Cancellation test timing updates
    • Increased retryDelay to allow proper cancellation testing during active retries
    • Removed focus-related cleanup logic
  • React/Solid test simplifications
    • Removed complex focus regain event dispatching
    • Simplified timing logic since retries no longer pause

Summary by CodeRabbit

  • New Features

    • Queries now continue retrying in the background when the page/tab is unfocused or hidden, improving reliability during inactivity.
    • Refetch intervals can continue running with retries in the background for uninterrupted data updates.
    • Added a new option to control background refetch intervals: refetchIntervalInBackground.
    • Cancelling a query during a retry now consistently surfaces a cancellation error.
  • Tests

    • Updated and expanded tests to cover background retries, refetch intervals while inactive, and cancellation during retries.

Copy link

nx-cloud bot commented Aug 14, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 39745de

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 2m 3s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 21s View ↗

☁️ Nx Cloud last updated this comment at 2025-08-14 08:50:26 UTC

Copy link

pkg-pr-new bot commented Aug 14, 2025

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9563

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9563

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9563

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9563

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9563

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9563

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9563

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9563

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9563

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9563

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9563

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9563

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9563

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9563

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9563

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9563

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9563

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9563

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9563

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9563

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9563

commit: 39745de

@TkDodo
Copy link
Collaborator

TkDodo commented Aug 14, 2025

plase have a look at the conflicts and failing tests

@Hellol77 Hellol77 force-pushed the fix/retry-background-focus-issue branch from 39745de to 7250804 Compare August 14, 2025 12:32
@Hellol77 Hellol77 force-pushed the fix/retry-background-focus-issue branch from 7250804 to 56049b5 Compare August 14, 2025 13:19
@Hellol77
Copy link
Author

Hellol77 commented Aug 14, 2025

I fixed the conflicts and test issues! thanks for review. @TkDodo

Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Tests across core and adapters were updated to expect retries and refetch intervals to continue while unfocused/hidden. Core retry logic removed focus gating. A public option refetchIntervalInBackground was added to QueryObserverOptions. New tests cover background retries and cancellation behavior.

Changes

Cohort / File(s) Summary
Core retry control flow
packages/query-core/src/retryer.ts
Removed focus dependency in canContinue; now proceeds based on networkMode/online status and canRun.
Public types
packages/query-core/src/types.ts
Added QueryObserverOptions.refetchIntervalInBackground?: boolean.
Core tests
packages/query-core/src/__tests__/query.test.tsx
Updated expectations for background retries; added tests for unfocused background retry, cancellation during retry, and refetchInterval with retries while tab hidden.
React adapter tests
packages/react-query/src/__tests__/useQuery.test.tsx
Adapted test to assert retries continue in background when page is hidden; removed focus-regain flow.
Solid adapter tests
packages/solid-query/src/__tests__/useQuery.test.tsx
Updated test to expect background retries while page is not focused; removed visibility-change pause/resume flow.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant QueryObserver
  participant Retryer
  participant onlineManager

  App->>QueryObserver: start query (refetchInterval, retry)
  QueryObserver->>Retryer: execute with retry config
  Retryer->>onlineManager: isOnline?
  alt networkMode == "always" or online
    Retryer-->>Retryer: schedule retries (tab may be hidden)
    Retryer-->>QueryObserver: success or error after retries
  else
    Retryer-->>QueryObserver: wait until online
  end
  QueryObserver-->>App: notify result (continues even when hidden)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Ensure retries proceed while tab is inactive when refetchIntervalInBackground is set (#8353)
Maintain retry count/delay semantics independent of tab visibility (#8353)
Continue refetchInterval cycles with retries during tab inactivity (#8353)

Poem

In moonlit tabs where focus wanes,
My queries hop through hidden lanes.
Retries thump in steady time,
Interval beats—four taps in rhyme.
No pause for shade, no fearful lag—
This bunny ships with background swag.
Carrot-logs green, no red flag!

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
packages/solid-query/src/__tests__/useQuery.test.tsx (3)

3203-3205: Ensure visibility state is restored even on test failure (wrap in try/finally)

If an assertion fails, the mocked visibility may bleed into subsequent tests. Wrap the body in try/finally to guarantee cleanup.

-    // make page unfocused
-    const visibilityMock = mockVisibilityState('hidden')
+    // make page unfocused
+    const visibilityMock = mockVisibilityState('hidden')
+    try {
@@
-    visibilityMock.mockRestore()
+    } finally {
+      visibilityMock.mockRestore()
+    }

Also applies to: 3255-3256


3209-3216: Avoid unnecessary generic on Promise.reject

Promise.reject<unknown>(...) doesn’t add value here and can confuse readers regarding the error type. Returning a rejected promise without the generic keeps intent clear while preserving the string error typing set for useQuery.

-        queryFn: () => {
+        queryFn: () => {
           count++
-          return Promise.reject<unknown>(`fetching error ${count}`)
+          return Promise.reject(`fetching error ${count}`)
         },

3237-3251: Optional: consolidate waits to reduce flakiness and execution time

You perform multiple sequential waitFor checks for related end-state assertions. Consolidating into one waitFor that verifies all required end-state texts reduces the number of polling cycles and makes the test slightly faster and less chatty.

-    await vi.waitFor(() =>
-      expect(rendered.getByText('failureCount 4')).toBeInTheDocument(),
-    )
-    await vi.waitFor(() =>
-      expect(
-        rendered.getByText('failureReason fetching error 4'),
-      ).toBeInTheDocument(),
-    )
-    await vi.waitFor(() =>
-      expect(rendered.getByText('status error')).toBeInTheDocument(),
-    )
-    await vi.waitFor(() =>
-      expect(rendered.getByText('error fetching error 4')).toBeInTheDocument(),
-    )
+    await vi.waitFor(() => {
+      expect(rendered.getByText('failureCount 4')).toBeInTheDocument()
+      expect(rendered.getByText('failureReason fetching error 4')).toBeInTheDocument()
+      expect(rendered.getByText('status error')).toBeInTheDocument()
+      expect(rendered.getByText('error fetching error 4')).toBeInTheDocument()
+    })
packages/react-query/src/__tests__/useQuery.test.tsx (1)

3434-3479: Guard visibility mock with try/finally to prevent cross-test leakage

If an assertion fails, visibilityMock.mockRestore() won't run and could affect subsequent tests. Wrap the test body in a try/finally.

   it('should continue retry in background even when page is not focused', async () => {
     const key = queryKey()

     // make page unfocused
     const visibilityMock = mockVisibilityState('hidden')
+    try {

       let count = 0

       function Page() {
         const query = useQuery<unknown, string>({
           queryKey: key,
           queryFn: () => {
             count++
             return Promise.reject<unknown>(`fetching error ${count}`)
           },
           retry: 3,
           retryDelay: 1,
         })

         return (
           <div>
             <div>error {String(query.error)}</div>
             <div>status {query.status}</div>
             <div>failureCount {query.failureCount}</div>
             <div>failureReason {query.failureReason}</div>
           </div>
         )
       }

       const rendered = renderWithClient(queryClient, <Page />)

       // With the new behavior, retries continue in background
       // Wait for all retries to complete
       await vi.advanceTimersByTimeAsync(4)
       expect(rendered.getByText('failureCount 4')).toBeInTheDocument()
       expect(
         rendered.getByText('failureReason fetching error 4'),
       ).toBeInTheDocument()
       expect(rendered.getByText('status error')).toBeInTheDocument()
       expect(rendered.getByText('error fetching error 4')).toBeInTheDocument()

       // Verify all retries were attempted
       expect(count).toBe(4)
-
-    visibilityMock.mockRestore()
+    } finally {
+      visibilityMock.mockRestore()
+    }
   })
packages/query-core/src/__tests__/query.test.tsx (4)

61-99: Solid background-retry assertion; add try/finally around visibility mock

The updated expectation aligns with removing focus gating. Minor robustness: ensure the visibility mock is restored even on failure.

-    const visibilityMock = mockVisibilityState('hidden')
+    const visibilityMock = mockVisibilityState('hidden')
+    try {
       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,
       })
 
       promise.then((data) => {
         result = data
       })
 
       // Check if we do not have a result initially
       expect(result).toBeUndefined()
 
       // With new behavior, retries continue in background
       // Wait for retries to complete
       await vi.advanceTimersByTimeAsync(10)
       expect(result).toBe('data3')
       expect(count).toBe(3)
-
-    visibilityMock.mockRestore()
+    } finally {
+      visibilityMock.mockRestore()
+    }

149-178: New background-retry test looks good; ensure visibility cleanup via try/finally

Matches the core change to remove focus gating. Add try/finally so the visibility mock is always restored.

-    const visibilityMock = mockVisibilityState('hidden')
+    const visibilityMock = mockVisibilityState('hidden')
+    try {
       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,
       })
       promise.then((data) => {
         result = data
       })
       // Check if we do not have a result initially
       expect(result).toBeUndefined()
       // Unlike the old behavior, retry should continue in background
       // Wait for retries to complete
       await vi.advanceTimersByTimeAsync(10)
       expect(result).toBe('data3')
       expect(count).toBe(3)
-    visibilityMock.mockRestore()
+    } finally {
+      visibilityMock.mockRestore()
+    }

180-217: Simplify cancellation assertion and remove redundant checks

You can assert the cancellation more directly and drop the extra result plumbing and redundant instanceof Error check.

-  it('should throw a CancelledError when a retrying query is cancelled', async () => {
+  it('should throw a CancelledError when a retrying query is cancelled', async () => {
     const key = queryKey()

     let count = 0
-    let result: unknown
 
     const promise = queryClient.fetchQuery({
       queryKey: key,
       queryFn: (): Promise<unknown> => {
         count++
         throw new Error(`error${count}`)
       },
       retry: 3,
-      retryDelay: 100, // Longer delay to allow cancellation
+      retryDelay: 100, // Longer delay to allow cancellation
     })
 
-    promise.catch((data) => {
-      result = data
-    })
-
     const query = queryCache.find({ queryKey: key })!
 
     // Wait briefly for first failure and start of retry
     await vi.advanceTimersByTimeAsync(1)
     expect(result).toBeUndefined()
 
     // Cancel query during retry
     query.cancel()
 
-    // Check if the error is set to the cancelled error
-    try {
-      await promise
-      expect.unreachable()
-    } catch {
-      expect(result instanceof CancelledError).toBe(true)
-      expect(result instanceof Error).toBe(true)
-    }
+    // Assert promise rejects with CancelledError
+    await expect(promise).rejects.toBeInstanceOf(CancelledError)
   })

Note: if you keep the current style, consider adding await vi.advanceTimersByTimeAsync(0) after query.cancel() to flush microtasks, but it isn’t strictly necessary when awaiting the promise.


258-294: Good coverage of refetchInterval + retries in background; add subscription cleanup and try/finally

  • Capture and call the unsubscribe returned from subscribe.
  • Wrap with try/finally to always destroy the observer and restore visibility, even if an assertion fails.
   it('should continue refetchInterval with retries in background when tab is inactive', async () => {
     const key = queryKey()
-    const visibilityMock = mockVisibilityState('hidden')
+    const visibilityMock = mockVisibilityState('hidden')
 
     let totalRequests = 0
 
     const queryObserver = new QueryObserver(queryClient, {
       queryKey: key,
       queryFn: () => {
         totalRequests++
         // Always fail to simulate network offline
         throw new Error(`Network error ${totalRequests}`)
       },
       refetchInterval: 60000,
       refetchIntervalInBackground: true,
       retry: 3,
       retryDelay: 1,
     })
 
-    queryObserver.subscribe(() => {})
+    const unsubscribe = queryObserver.subscribe(() => {})
 
-    // First interval: t=0 to t=60s (initial query + retries)
-    await vi.advanceTimersByTimeAsync(60000)
-    expect(totalRequests).toBe(4)
-
-    // Second interval: t=60s to t=120s (refetch + retries)
-    await vi.advanceTimersByTimeAsync(60000)
-    expect(totalRequests).toBe(8)
-
-    // Third interval: t=120s to t=180s (refetch + retries)
-    await vi.advanceTimersByTimeAsync(60000)
-    expect(totalRequests).toBe(12)
-
-    queryObserver.destroy()
-    visibilityMock.mockRestore()
+    try {
+      // First interval: t=0 to t=60s (initial query + retries)
+      await vi.advanceTimersByTimeAsync(60000)
+      expect(totalRequests).toBe(4)
+
+      // Second interval: t=60s to t=120s (refetch + retries)
+      await vi.advanceTimersByTimeAsync(60000)
+      expect(totalRequests).toBe(8)
+
+      // Third interval: t=120s to t=180s (refetch + retries)
+      await vi.advanceTimersByTimeAsync(60000)
+      expect(totalRequests).toBe(12)
+    } finally {
+      unsubscribe()
+      queryObserver.destroy()
+      visibilityMock.mockRestore()
+    }
   })
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f1e608b and b4c70f1.

📒 Files selected for processing (4)
  • packages/query-core/src/__tests__/query.test.tsx (6 hunks)
  • packages/query-core/src/retryer.ts (0 hunks)
  • packages/react-query/src/__tests__/useQuery.test.tsx (3 hunks)
  • packages/solid-query/src/__tests__/useQuery.test.tsx (3 hunks)
💤 Files with no reviewable changes (1)
  • packages/query-core/src/retryer.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/query-core/src/__tests__/query.test.tsx (3)
packages/query-core/src/queriesObserver.ts (1)
  • result (186-201)
packages/query-core/src/query.ts (1)
  • promise (198-200)
packages/query-core/src/retryer.ts (1)
  • CancelledError (57-65)
🔇 Additional comments (1)
packages/solid-query/src/__tests__/useQuery.test.tsx (1)

3200-3217: Test intent and expectations align with the new background-retry semantics

Good addition. The scenario accurately verifies that retries are not gated by focus anymore: failureCount/error state match the configured retry count (initial + 3 retries = 4). Assertions cover both failureCount and surfaced error, which is helpful.

Also applies to: 3235-3251

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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