Skip to content

[BUG] React Query double fetching in Refine v5 caused by unintended signal access (v4 did not do this) #7132

@amerryma

Description

@amerryma

Describe the bug

Refine v5’s useList performs two HTTP requests in React 18 + Strict Mode in development, while refine v4 performs only one.

This is not caused by Strict Mode alone.
The root cause is that refine v5 accidentally opts into React Query abort/cancel semantics by reading the context.signal getter during queryFn execution via a spread operation:

const meta = {
  ...combinedMeta,
  ...prepareQueryContext(context), // <-- this spread triggers context.signal
};

Spreading an object with a signal getter invokes the getter, causing React Query to detect that signal is used. When signal is used, React Query treats the query as abortable, and in dev + Strict Mode it cancels the first run and issues a second fetch.

This behavior did not happen in refine v4 because v4 passed the signal getter lazily (meta.queryContext) without accessing it inside queryFn.

This explains why:

  • v4 = 1 request
  • v5 = 2 requests
  • Patching out the spread = v5 goes back to 1 request

Steps To Reproduce

  1. Create two minimal refine apps (one using refine v4, one using refine v5).
    My reproduction repo structure:
    .
    ├── refine-v4
    │   ├── src/App.jsx
    │   ├── index.html
    │   └── package.json
    └── refine-v5
        ├── src/App.jsx
        ├── index.html
        └── package.json
    
  2. Both apps use the same simple <useList> component:
    const { data, isLoading } = useList({ resource: "posts" });
  3. Run both apps in React 18 with Strict Mode enabled (default Vite setup).
  4. Open DevTools → Network tab
  5. In refine v4, you will see one network request.
  6. In refine v5, you will see two network requests.
  7. Now patch refine v5’s compiled JS:
    Replace:
    const meta2 = {
      ...combinedMeta,
      ...prepareQueryContext(context)
    };
    With:
    const meta2 = {
      ...combinedMeta,
      queryKey: context.queryKey
    };
    
    Object.defineProperty(meta2, "signal", {
      enumerable: true,
      get() {
        return context.signal;
      },
    });
  8. Restart refine v5. The second request disappears.

Expected behavior

Refine v5 should behave like refine v4:

  • One request in development + Strict Mode if the data provider does not explicitly opt in to abort controller semantics.
  • context.signal should remain lazy and not be accessed inside queryFn until a data provider actually uses it.

Packages

Refine v4:

yarn why @refinedev/core
└─ refine-v4-test@workspace:.
   └─ @refinedev/core@npm:4.58.0 [53edc] (via npm:^4.49.0 [53edc])

Refine v5:

yarn why @refinedev/core
└─ refine-v5-test@workspace:.
   └─ @refinedev/core@npm:5.0.6 [1e1cb] (via npm:^5.0.0 [1e1cb])

Additional Context

  • This behavior is fully explained by the TanStack Query team here:
    useQuery runs twice on initial load with React 18 TanStack/query#3633
  • Reading or destructuring signal marks the query as abortable.
  • In refine v5, the spread operator ...prepareQueryContext(context) invokes the signal getter, which implicitly accesses context.signal. That is the only difference from refine v4.
  • Strict Mode double-render alone does not cause duplicate network requests unless the query is opt-in abortable.
  • Two older refine issues referenced Strict Mode but did not identify the real cause:
    [BUG] - Requests are being called twice #3449
    [BUG] Request api two times #2978
  • A minimal and backwards-compatible fix is simply avoiding the spread of an object with signal getter properties, and instead defining the lazy getter directly on meta, matching v4 behavior.

If needed, I can also provide a PR updating useList, useOne, useMany, etc., to ensure the signal getter is not invoked unless explicitly requested by the data provider.

Here is an example repo demonstrating the behavior: https://github.com/amerryma/refine-react-query-v4-v5-bug

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions