Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[🐞] useResource$ writes each new result into the previously returned object

Open
#9,308 1 comment 0 reactions 0 assignees View on GitHub

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
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);
  }
}
  • createStore uses the first result itself as the store's target, so the later Object.assign writes through to the caller's object.
  • Object.assign only 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

https://next.qwik.dev/playground/#buildMode=development&entryStrategy=segment&f=5VdNa9tAEL33V0xDYCUQCk3oocJxSNoeWkpLyTEEItnrWPGHjGXXMY7%2Be9%2FMaldrxU5i6K0%2BGLTanZmdj%2FeevKY5Pfv0cVfXRA4rIuTO9Ge01azyZAj6BeEC3MfYT%2FvAHqBKMRf%2B6aU9ppphuqChvGO4YPYpGb%2BK7AGgSItVDjYIUobl2fD3D4YYYQU%2BHFFKEz0pwncm1cbiuWQ5TXATRsKE1OcUKHmpcBvGv4Ru1GiaDzQW1P24%2BKNLdRsB7FLURfcTg9tVBCNZ28iVbwSkwgikbnkzEvqmOTKRjvQacTbaRqWKnkhl%2FM9w3g34H3WyB%2BRqX5aTmX8sUApb3J6seDRvGygx6Btsar1SuTBcIDgh7wKEJP6IUC22AOZBTaDPkNM668V0vI5A6yOwAQuiTA8BYKhCNi9WrG1KXZaYLTGTDyhwVPP0RO9Hoe19vpzxVS%2FI9W5Gt7zIePLCmJx1%2FRvSao56QK7BgHC%2F4U5WP4VEXvcRlIExCAWUabSgxgSdOauzZniYjJh42Xx9pqROr%2BjrrkQZpx30NZ4IfeyvZ3Y9LdnzGvJDx2TaBh1egiQ2ilTVjKlst31nT6MOsZtcxOVGXLR3Izk3pqe4bjEICS17TmijsOpeD6FLLp1aP8hA5gxcvdmA601n5vv1r5%2Bxkf35YG02hDD8bVrOuBqy0NjfUQUOYZq08mWr7laIqXgJzY7Wb%2FYRQqwbuI8gZYuJ1NsCc%2FV3Rt4%2FPyrZ3VHj3vw2crS1SHRBd8fmVczYUCVkHwUa4geotIDBJayON8rWVzH2mQRdkKLAroeKgC6qunvmB8vTYoEJw7dTrbm9bJgGMXfwAw%2BbLR4FuNR2ICTlylKDo%2B6mVb0Ke%2BegGbu9GHu8Ms59T9IGaIum4boxfX3kCvN3BgNDb7FMx0nd6QaGExLwjchAr5eIegIadye%2Bvxedc7N6zrc8XiVQ24LUtYOYIAfbcTW7nkfEVGB%2Fme6lAKA9wwsxO9YDACVjnrDhpRIdD0yyU33QxSSrlN5DY%2B%2B53usJdffdHicLQK9e%2F9DA21O%2BozD1K5M0iDQIgHvdbxemhbdILr51fY%2BGkkshoxgSQ8%2FoFIxQ4BONl%2FdwgZRkyZrPEEJEq2EO%2BoD9XX7bWcsXmMbB7qR0Tuy8NEprvzz%2B4IkuT4DqdLIlj%2B3iM5m8pbP%2BsUhuOd0nlrHjvxDLfwE

Steps to reproduce

The page lists these steps with the expected and actual result of each.

  1. Click Show A. It shows Case A: knife, gloves (featured). Correct.
  2. Click Show B. It shows Case B: sticker (featured). Case B has no featured key; it was left over from A.
  3. Click Show A again. It shows Case B: sticker (featured). Expected Case A.
  4. Click Inspect cache. cache.a is 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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from QwikDev/qwik

All issues in QwikDev/qwik

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.