[🐞] useResource$ writes each new result into the previously returned object
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 63/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript, typescript
- Domain
- frontend
Research direction
Start with _rsc in packages/qwik/src/core/use/use-resource.ts and trace how it creates and updates the resource store for object and array results. Check the existing resource tests for coverage of repeated results and SSR hydration. Done means each run displays exactly its returned keys and leaves objects returned by the resource function unchanged.
Written by the indexing model from the issue text.
Description
Which component is affected?
Qwik Runtime
Describe the bug
useResource$ writes each new result into the object an earlier run returned. If that object is shared (a GraphQL client cache, a memoized lookup, a module constant), the shared object is overwritten, and later readers get the wrong data.
A second symptom also shows when the first result came from SSR: keys that a newer result doesn't have are kept from the older one.
Both come from _rsc in packages/qwik/src/core/use/use-resource.ts:
if (result && typeof result === 'object') {
if (ref.r) {
Object.assign(ref.r, result);
} else {
ref.r = createStore(fn.$container$, result, StoreFlags.RECURSIVE);
}
}
createStoreuses the first result itself as the store's target, so the laterObject.assignwrites through to the caller's object.Object.assignonly copies keys that exist on the new result, so keys only on the old result remain.
Expected: each run's result is shown as returned, and the objects a resource function returns aren't modified.
Reproduction
Steps to reproduce
The page lists these steps with the expected and actual result of each.
- Click Show A. It shows
Case A: knife, gloves (featured). Correct. - Click Show B. It shows
Case B: sticker (featured). Case B has nofeaturedkey; it was left over from A. - Click Show A again. It shows
Case B: sticker (featured). Expected Case A. - Click Inspect cache.
cache.ais now{"name":"Case B","items":["sticker"],"featured":true}. The module's own data was overwritten in step 2.
The resource returns null on the server, so its first object result is created on the client. That's when the store wraps the caller's object directly. If the server resolves first (initial key 'a', no isServer check), the store wraps a deserialized copy instead: cache survives, but step 2 still shows the leftover (featured).
The route component:
import { component$, isServer, Resource, useResource$, useSignal } from '@qwik.dev/core';
// Stands in for any cache that hands out the same object twice (a GraphQL client cache, a memo)
const cache = {
a: { name: 'Case A', items: ['knife', 'gloves'], featured: true },
b: { name: 'Case B', items: ['sticker'] },
};
export default component$(() => {
const key = useSignal<'a' | 'b' | null>(null);
const cacheDump = useSignal('');
const box = useResource$(async ({ track }) => {
const k = track(key);
// Resolves on the client only, like data behind a browser session
if (isServer || !k) return null;
return cache[k];
});
return (
<>
<h3>useResource$ writes each new result into the object it returned before</h3>
<p>
The resource returns <code>cache.a</code> or <code>cache.b</code> as they are. Case B has no{' '}
<code>featured</code> key.
</p>
<button onClick$={() => (key.value = 'a')}>Show A</button>
<button onClick$={() => (key.value = 'b')}>Show B</button>
<button onClick$={() => (cacheDump.value = JSON.stringify(cache))}>Inspect cache</button>
<p>
Shown:{' '}
<Resource
value={box}
onResolved={(value) => (
<strong id="shown">
{value
? `${value.name}: ${value.items.join(', ')}${'featured' in value ? ' (featured)' : ''}`
: 'nothing selected'}
</strong>
)}
/>
</p>
<pre id="cache">{cacheDump.value}</pre>
<ol>
<li>
Click <b>Show A</b>. Expected and actual: <code>Case A: knife, gloves (featured)</code>
</li>
<li>
Click <b>Show B</b>. Expected: <code>Case B: sticker</code>. Actual: <code>Case B: sticker (featured)</code>,
because <code>featured</code> is left over from A's result.
</li>
<li>
Click <b>Show A</b> again. Expected: <code>Case A: knife, gloves (featured)</code>. Actual:{' '}
<code>Case B: sticker (featured)</code>.
</li>
<li>
Click <b>Inspect cache</b>. Expected: <code>cache</code> unchanged. Actual: <code>cache.a</code> is now
Case B's data. Step 2 wrote B's result into the object A's run returned, which is <code>cache.a</code>{' '}
itself.
</li>
</ol>
</>
);
});
Possible fix (I haven't checked the reactivity implications): build the first store over a shallow copy of the result, and on later runs replace its keys rather than merge them (delete keys the new result doesn't have, then assign). Arrays need the same treatment when the result switches between array and object.
System Info
@qwik.dev/core: 2.0.0-rc.4-dev+3a5105f (next.qwik.dev playground, main 024824521); also pkg.pr.new 224091137 and d1ca2412e
- Dominant language
- TypeScript
- Stars
- 22.1k
- Forks
- 1.4k
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 123
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from QwikDev/qwik
-
[🐞] Production SSR render errors are never logged or passed to `next()` (node middleware returns on `headersSent` first)Possibly taken @bunnysayzz claimed this 16 days ago. OpenIMPORTANT
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
QwikDev/qwik#9191 · 6 comments ·
Maintainers usually reply within 1 day
-
docs router V2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
DX enhancement V2
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
QwikDev/qwik#8480 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
QwikDev/qwik#9310 · 2 comments ·
Maintainers usually reply within 1 day
-
[🐞] Nested `.map()` handler captures still hoisted to component scope when the component returns JSX directly (#9190)Possibly taken @tuurbo claimed this 3 days ago. OpenIMPORTANT
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
CopilotKit/openmuse#215 ·
Maintainers usually reply within 3 days
-
b:minor bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
software-mansion/argent#1698 ·
Maintainers usually reply within 1 day
-
e2e-failure ready-to-code
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
redhat-developer/rhdh-plugin-export-overlays#4624 · 1 comment ·
Maintainers usually reply within 1 day