-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
fix(nuxt): prevent duplicate execution on key change in useAsyncData
#33325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
@nuxt/kit
nuxt
@nuxt/rspack-builder
@nuxt/schema
@nuxt/vite-builder
@nuxt/webpack-builder
commit: |
WalkthroughThe diff updates packages/nuxt/src/app/composables/asyncData.ts to import Vue's queuePostFlushCb, introduce a local keyChanging flag, and rework the key watcher to read existing data from the old key, initialise the new key with preserved or cached data, and perform the actual key switch after the current flush. Migration logic can trigger fetch for the new key depending on prior state, cancels/unregisters the old key safely, and resets the concurrency guard via queuePostFlushCb. The params watcher short-circuits while keyChanging is true. Scope disposal now unregisters both the key and params watchers. Two regression tests were added in test/nuxt/use-async-data.test.ts. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (2)**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.{test,spec}.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
🧠 Learnings (2)📓 Common learnings📚 Learning: 2025-09-10T14:42:56.647ZApplied to files:
🧬 Code graph analysis (1)test/nuxt/use-async-data.test.ts (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
🔇 Additional comments (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
340-348: Optional: mirror migrated value into payload for consistencyWhen seeding the new key from oldKey’s in-memory value (hadData), consider also setting nuxtApp.payload.data[newKey] to keep payload/devtools in sync until the next fetch.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/nuxt/src/app/composables/asyncData.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/app/composables/asyncData.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.
📚 Learning: 2025-09-10T14:42:56.647Z
Learnt from: Tofandel
PR: nuxt/nuxt#33192
File: test/nuxt/use-async-data.test.ts:366-373
Timestamp: 2025-09-10T14:42:56.647Z
Learning: In the Nuxt useAsyncData test "should watch params deeply in a non synchronous way", the foo watcher intentionally updates both params.foo and params.locale using locale.value, simulating a scenario where one watcher consolidates multiple reactive values into a shared params object for testing debounced/non-synchronous behavior.
Applied to files:
packages/nuxt/src/app/composables/asyncData.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
- GitHub Check: test-fixtures (windows-latest, built, rspack, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
- GitHub Check: typecheck (windows-latest, bundler)
- GitHub Check: test-size
- GitHub Check: test-benchmark
- GitHub Check: code
🔇 Additional comments (5)
packages/nuxt/src/app/composables/asyncData.ts (5)
1-1: Import of queuePostFlushCb is appropriateUsing queuePostFlushCb to reset guards post-flush is the right tool for this watcher timing issue.
196-196: Guard flag to prevent overlapping executesIntroducing keyChanging is a simple and effective concurrency guard during key migration.
327-366: Key migration ordering and post-flush guard look correct
- Creating the new container before unregistering the old one avoids data loss.
- Passing cachedData only when sourced from getCachedData is sound.
- Post-flush reset via queuePostFlushCb prevents params watcher from double-firing in the same cycle.
Based on learnings
358-360: Confirm behaviour: executing on key migration when immediate is falseTriggering execute when hadData or wasRunning even if options.immediate is false may surprise consumers expecting no automatic fetches. Please confirm this matches desired semantics and existing tests cover it.
379-383: Proper cleanup of both watchersUnsubscribing both key and params watchers on scope dispose is correct and prevents leaks.
| onScopeDispose(() => { | ||
| unsubKeyWatcher() | ||
| unsubWatcher() | ||
| unsubParamsWatcher() | ||
| unregister(key.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: params watcher calls a stale container after a key change
The params watcher invokes asyncData._execute, which is bound to the initial container. After a key change, it should target the current container or it may no longer refresh correctly.
Apply this diff:
- ? watch(options.watch, () => {
- if (keyChanging) { return } // avoid double execute while the key switch is being processed
- asyncData._execute({ cause: 'watch', dedupe: options.dedupe })
- })
+ ? watch(options.watch, () => {
+ if (keyChanging) { return } // avoid double execute while the key switch is being processed
+ nuxtApp._asyncData[key.value]!._execute({ cause: 'watch', dedupe: options.dedupe })
+ })🤖 Prompt for AI Agents
In packages/nuxt/src/app/composables/asyncData.ts around lines 371-375, the
watcher callback currently calls the originally bound asyncData._execute which
sticks to the initial container; change the callback to resolve the
current/active asyncData container at invocation time and call that container's
_execute with the same args (i.e., avoid invoking a pre-bound method tied to the
old container). Implement this by looking up the live container on each watch
trigger (via the internal accessor the module exposes for the active container
or by reading the mutable container reference on asyncData) and invoking
container._execute({ cause: 'watch', dedupe: options.dedupe }) only on that
resolved container.
CodSpeed Performance ReportMerging #33325 will not alter performanceComparing Summary
|
|
thank you! ❤️ would you add a regression test in |
|
Sure!! |
|
Hi! @danielroe! I've added the regression tests as requested 🙏 What the tests do:
Key test case: // This watch before useAsyncData triggered the bug
watch(q, () => {})
const { data } = await useAsyncData(
() => `query-${q.value}`, // Computed key
() => handler(q.value),
{ watch: [q], immediate: true }
) |
useFetch
useFetchuseAsyncData
🔗 Linked issue
Fixes #33274
resolves #33240
resolves #33369
When two
useFetchcalls generate the same key and are refreshed by reactive data changes,one of them may throw:
📚 Description
This happens because both the key watcher and the params watcher can trigger
execute()in the same flush cycle, leading to overlapping requests.Changes
keyChangingguard that is set during a key migrationqueuePostFlushCbafter the current flushWhy
execute()during key switchesflush: 'sync'→ immediate and deterministicpre) → batched, non-synchronous (covered by tests)This resolves the regression reported in #33274 while keeping the new test
(
should watch params deeply in a non synchronous way) passing.