fix(nuxt): update `useFetch` key even with `watch: false` by danielroe · Pull Request #35002 · nuxt/nuxt · GitHub
Skip to content

fix(nuxt): update useFetch key even with watch: false - #35002

Merged
danielroe merged 3 commits into
mainfrom
fix/usefetch-stale-key
May 11, 2026
Merged

danielroe merged 3 commits into
mainfrom
fix/usefetch-stale-key

Conversation

@danielroe

Copy link
Copy Markdown
Member

🔗 Linked issue

resolves #32102
resolves #32437

📚 Description

with watch: false, useFetch was unwrapping the key to a plain string before passing it to useAsyncData, which froze the 'key'

that's what's behind both linked issues: getCachedData always saw the original key, and two useFetch calls that started out with the same shape kept sharing one async data slot even after their inputs diverged - so execute() on one overwrote the other's data.

this came from #32019, which fixed watch: false not preventing auto-refetch (#32016) but disabled key reactivity entirely.

so this pr passes the reactive key through, and adds an internal _keyTriggersExecute flag on useAsyncData's key watcher. when false, the data slot is still migrated to the new key (so getCachedData and manual execute() see it), but the auto-execute on key change is skipped. useFetch sets it to false whenever watch: false.

reactive query/params still don't trigger auto-refetch.

@danielroe
danielroe requested a review from OrbisK May 7, 2026 20:24
@bolt-new-by-stackblitz

Copy link
Copy Markdown

@github-actions github-actions Bot added 5.x 🐛 bug Something isn't working as expected labels May 7, 2026
@coderabbitai

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/nuxt/use-fetch.test.ts`:
- Around line 372-390: Two effectScope instances (scope1 and scope2 created via
effectScope() and run(createScope())) are not stopped, risking cross-test
leakage; fix by calling scope1.stop() and scope2.stop() after the test actions
(after the awaits and assertions) to dispose watchers/deps created by
createScope(); ensure you add the stop calls referencing the exact variables
scope1 and scope2 so both scopes are cleaned up regardless of assertion outcomes
(or wrap in finally if you prefer deterministic teardown).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 00b8dc6c-6e56-4ca9-8ea2-10b9bc232a37

📥 Commits

Reviewing files that changed from the base of the PR and between 8630e8f and d97875b.

📒 Files selected for processing (3)
  • packages/nuxt/src/app/composables/asyncData.ts
  • packages/nuxt/src/app/composables/fetch.ts
  • test/nuxt/use-fetch.test.ts

Comment on lines +372 to +390
const scope1 = effectScope()
const { data: d1, id: i1, query: q1, execute: e1 } = scope1.run(createScope())!
const scope2 = effectScope()
const { data: d2 } = scope2.run(createScope())!

expect.soft(d1.value).toStrictEqual(undefined)
expect.soft(d2.value).toStrictEqual(undefined)

q1.value.someInput = 'test'
i1.value++
await e1()
await flushPromises()
await nextTick()
await flushPromises()

// d1 should have data; d2 should remain untouched
expect.soft(d1.value).toMatchObject({ url: '/api/key/2' })
expect.soft(d2.value).toStrictEqual(undefined)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stop created effect scopes to avoid cross-test leakage.

At Line 372 and Line 374, two effectScope() instances are created but never disposed. This can leak watchers/deps into subsequent tests and make outcomes order-dependent.

Proposed fix
   const scope1 = effectScope()
   const { data: d1, id: i1, query: q1, execute: e1 } = scope1.run(createScope())!
   const scope2 = effectScope()
   const { data: d2 } = scope2.run(createScope())!

-  expect.soft(d1.value).toStrictEqual(undefined)
-  expect.soft(d2.value).toStrictEqual(undefined)
+  try {
+    expect.soft(d1.value).toStrictEqual(undefined)
+    expect.soft(d2.value).toStrictEqual(undefined)

-  q1.value.someInput = 'test'
-  i1.value++
-  await e1()
-  await flushPromises()
-  await nextTick()
-  await flushPromises()
+    q1.value.someInput = 'test'
+    i1.value++
+    await e1()
+    await flushPromises()
+    await nextTick()
+    await flushPromises()

-  // d1 should have data; d2 should remain untouched
-  expect.soft(d1.value).toMatchObject({ url: '/api/key/2' })
-  expect.soft(d2.value).toStrictEqual(undefined)
+    // d1 should have data; d2 should remain untouched
+    expect.soft(d1.value).toMatchObject({ url: '/api/key/2' })
+    expect.soft(d2.value).toStrictEqual(undefined)
+  } finally {
+    scope1.stop()
+    scope2.stop()
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const scope1 = effectScope()
const { data: d1, id: i1, query: q1, execute: e1 } = scope1.run(createScope())!
const scope2 = effectScope()
const { data: d2 } = scope2.run(createScope())!
expect.soft(d1.value).toStrictEqual(undefined)
expect.soft(d2.value).toStrictEqual(undefined)
q1.value.someInput = 'test'
i1.value++
await e1()
await flushPromises()
await nextTick()
await flushPromises()
// d1 should have data; d2 should remain untouched
expect.soft(d1.value).toMatchObject({ url: '/api/key/2' })
expect.soft(d2.value).toStrictEqual(undefined)
})
const scope1 = effectScope()
const { data: d1, id: i1, query: q1, execute: e1 } = scope1.run(createScope())!
const scope2 = effectScope()
const { data: d2 } = scope2.run(createScope())!
try {
expect.soft(d1.value).toStrictEqual(undefined)
expect.soft(d2.value).toStrictEqual(undefined)
q1.value.someInput = 'test'
i1.value++
await e1()
await flushPromises()
await nextTick()
await flushPromises()
// d1 should have data; d2 should remain untouched
expect.soft(d1.value).toMatchObject({ url: '/api/key/2' })
expect.soft(d2.value).toStrictEqual(undefined)
} finally {
scope1.stop()
scope2.stop()
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/nuxt/use-fetch.test.ts` around lines 372 - 390, Two effectScope
instances (scope1 and scope2 created via effectScope() and run(createScope()))
are not stopped, risking cross-test leakage; fix by calling scope1.stop() and
scope2.stop() after the test actions (after the awaits and assertions) to
dispose watchers/deps created by createScope(); ensure you add the stop calls
referencing the exact variables scope1 and scope2 so both scopes are cleaned up
regardless of assertion outcomes (or wrap in finally if you prefer deterministic
teardown).

@pkg-pr-new

pkg-pr-new Bot commented May 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@35002

@nuxt/nitro-server

npm i https://pkg.pr.new/@nuxt/nitro-server@35002

nuxt

npm i https://pkg.pr.new/nuxt@35002

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@35002

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@35002

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@35002

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@35002

commit: 31ab871

@codspeed

codspeed Bot commented May 7, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing fix/usefetch-stale-key (31ab871) with main (778f190)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@danielroe danielroe self-assigned this May 7, 2026

@OrbisK OrbisK May 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cant this be an "official" option for async data (AsyncDataOptions)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean, watch: false?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you think it's a good idea, let's open a new PR to add the feature, but we can still get this one in as a fix for the next patch

@danielroe
danielroe merged commit 68ebb99 into main May 11, 2026
31 checks passed
@danielroe
danielroe deleted the fix/usefetch-stale-key branch May 11, 2026 11:25
This was referenced May 11, 2026
@github-actions github-actions Bot mentioned this pull request May 11, 2026
davidstackio pushed a commit to davidstackio/nuxt that referenced this pull request May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5.x 🐛 bug Something isn't working as expected

Projects

None yet

2 participants