useResource$ race conditions when pagination key changes rapidly
0 reputation · 18 Feb 2021, 04:16 UTC
0 reputation · 18 Feb 2021, 04:16 UTC
In Qwik v1.x, useResource$ deduplicates requests by key within a single render pass but does not expose an AbortSignal to the fetcher when the component unmounts or the key changes. When implementing cursor- or offset-based pagination, rapid key updates (for example, a user clicking "Next" multiple times or a debounced search input) leave in-flight requests for previous pages running to completion.
Those stale responses can arrive after the newer page's data, overwriting the correct state and causing UI flicker or corrupted lists. The framework RFCs discuss a usePaginatedResource$ helper and standard abort integration, but as of v1.5 no official primitive exists.
LIMIT/OFFSET or cursor bounds; the issue is client-side request lifecycle.AbortController wiring is possible but must be coordinated across SSR hydration and client navigation where the same loader may execute twice.streamResponse does not mitigate the race because the loader still returns a full page payload per key.What patterns reliably cancel stale useResource$ fetchers on key change without breaking resumability? How should a custom pagination hook coordinate AbortController across server and client loader executions? Are there plans to surface a cancellation token in the useResource$ fetcher signature?
The reliable pattern is to own an AbortController in a useSignal, abort it synchronously when the pagination key changes, and have the loader check signal.aborted before mutating state. Qwik v1.x does not yet surface a cancellation token in the useResource$ fetcher signature, so the controller must be threaded manually through the loader context and serialized across SSR hydration.
AbortController (or null).controller?.abort(), then assign a fresh AbortController to the signal.signal into the async function you give to useResource$ (via closure or a wrapper).if (signal.aborted) return before any state write.signal.aborted again and discard the payload if true.const abortCtrl = useSignal<AbortController | null>(null);
const data = useResource$(
async (key: string) => {
abortCtrl.value?.abort();
const ctrl = new AbortController();
abortCtrl.value = ctrl;
const res = await fetch(`/api/items?cursor=${key}`, { signal: ctrl.signal });
if (ctrl.signal.aborted) return; // stale
return res.json();
},
paginationKey
);
During server rendering, the loader runs once per key. Serialize a unique request ID (e.g., crypto.randomUUID()) into the initial HTML payload. On client mount, read that ID, create a matching AbortController, and store it in the same signal. If the key changes before hydration completes, the server‑side loader has already returned; the client‑side abort prevents the duplicated loader execution from overwriting state.
// Server loader
export const useItemsLoader = (key: string) => {
const requestId = crypto.randomUUID();
// embed requestId in response meta for client
return fetch(`/api/items?cursor=${key}`).then(r => r.json());
};
// Client hook
useClientEffect$(() => {
const ssrId = document.querySelector('[data-request-id]')?.dataset.requestId;
if (ssrId) {
// associate with current controller if needed
}
});
useResource$ SignatureAs of Qwik 1.5 there is no built‑in AbortSignal parameter. The RFC for usePaginatedResource$ and first‑class abort integration remains open. Track the Qwik repository issues for 'abort signal in useResource$' — the design discussion centers on whether the token should be injected via the loader context or added as a second argument to the fetcher callback.
console.log('aborted', signal.aborted) at the top of your fetcher; trigger rapid 'Next' clicks and confirm the log appears before any network response.useClientEffect$ hydration.try/catch that ignores AbortError.Are you using routeLoader$ or routeAction$ for the pagination fetch, or is the fetch entirely inside a component‑level useResource$? The coordination strategy differs slightly because route loaders run during navigation and have their own lifecycle.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.