refine icon indicating copy to clipboard operation
refine copied to clipboard

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

Open amerryma opened this issue 1 month ago • 6 comments

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: https://github.com/TanStack/query/issues/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: https://github.com/refinedev/refine/issues/3449 https://github.com/refinedev/refine/issues/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

amerryma avatar Nov 25 '25 17:11 amerryma

Hello @amerryma, thanks for the detailed issue. Would you like to create a PR?

BatuhanW avatar Nov 26 '25 10:11 BatuhanW

Interesting, it was a regression that was originally resolved here: https://github.com/refinedev/refine/pull/5851

We'll want to make sure we check out why the tests never caught this.

amerryma avatar Nov 26 '25 15:11 amerryma

Hello @amerryma any updates?

BatuhanW avatar Dec 08 '25 07:12 BatuhanW

Hello, I would like to work on this issue. I will begin implementing a fix and submit a PR once it’s ready.

Girma35 avatar Dec 09 '25 17:12 Girma35

Hello @amerryma any updates?

I have not had any time to work through this yet, it's pretty low priority for me because it only affects local dev.

amerryma avatar Dec 09 '25 17:12 amerryma

This is preventing us from upgrading to Refine v5, as it breaks our permission checks in dev. For some reason, the double call to retrieve someone's abilities causes 401s / other errors (the first one works properly, also in non-dev), resulting in the app not being usable in dev, making it quite difficult to build new things/fix issues.

JulienZD avatar Dec 09 '25 17:12 JulienZD