-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
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
- 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 - Both apps use the same simple
<useList>component:const { data, isLoading } = useList({ resource: "posts" });
- Run both apps in React 18 with Strict Mode enabled (default Vite setup).
- Open DevTools → Network tab
- In refine v4, you will see one network request.
- In refine v5, you will see two network requests.
- Now patch refine v5’s compiled JS:
Replace:With:const meta2 = { ...combinedMeta, ...prepareQueryContext(context) };
const meta2 = { ...combinedMeta, queryKey: context.queryKey }; Object.defineProperty(meta2, "signal", { enumerable: true, get() { return context.signal; }, });
- 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.signalshould remain lazy and not be accessed insidequeryFnuntil 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
signalmarks the query as abortable. - In refine v5, the spread operator
...prepareQueryContext(context)invokes thesignalgetter, which implicitly accessescontext.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
signalgetter properties, and instead defining the lazy getter directly onmeta, 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