-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
fix(nuxt): async-data executes should return new promise on deduplication #33740
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/nitro-server
nuxt
@nuxt/rspack-builder
@nuxt/schema
@nuxt/vite-builder
@nuxt/webpack-builder
commit: |
CodSpeed Performance ReportMerging #33740 will not alter performanceComparing Summary
|
WalkthroughThis pull request modifies error handling in the async data composable by updating three catch block branches to return the existing per-key promise rather than undefined. These changes affect scenarios where the current promise is replaced, where asyncData is aborted internally via dedupe or clear, and where an AbortError occurs. Accompanying this, a new test case is added to verify that multiple concurrent useAsyncData calls with the same key properly resolve to the same result when deduplication is enabled. Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
749-769: Lines 757 and 763 require guards to prevent returningundefinedpromises.The first catch branch (line 751–752) safely guards against undefined with
if (nuxtApp._asyncDataPromises[key] && nuxtApp._asyncDataPromises[key] !== promise), but lines 757 and 763 returnnuxtApp._asyncDataPromises[key]directly without guarding.If
clearNuxtDataexecutes between the promise assignment (line 778) and the catch block, it sets the key toundefined(line 625), causing lines 757 and 763 to returnundefinedinstead of a valid promise. This breaks the promise chain and contradicts the guard logic applied at line 751.Fix: Apply the same existence check at lines 757 and 763:
- Line 757:
if (nuxtApp._asyncDataPromises[key]) { return nuxtApp._asyncDataPromises[key] }- Line 763:
if (nuxtApp._asyncDataPromises[key]) { return nuxtApp._asyncDataPromises[key] }Alternatively, return the fallback promise variable instead of the cache lookup.
🧹 Nitpick comments (2)
test/nuxt/use-async-data.test.ts (1)
1157-1173: Clarify the expected behaviour withdedupe: 'cancel'.The test expects
promiseFnto be called 4 times withdedupe: 'cancel'. This is correct because each new call cancels the previous request and starts a fresh one. However, this differs from the objective of "deduplication" described in the PR.The test validates that even though earlier promises are cancelled, they still resolve correctly (by returning the per-key promise) rather than remaining unresolved or returning
undefined. Consider adding a comment explaining this behaviour to distinguish it fromdedupe: 'defer'(line 362) where the function is called only once.it('should correctly return deduped promises', async () => { vi.useFakeTimers() const promiseFn = vi.fn(() => new Promise(resolve => setTimeout(() => resolve('test'), 100))) + // With dedupe: 'cancel', each call cancels the previous one, but all promises + // should still resolve by returning the current per-key promise const p1 = useAsyncData('sameKey', promiseFn, { dedupe: 'cancel' })test/fixtures/basic/app/components/AsyncData.vue (1)
11-14: Consider documenting the hardcoded key usage.The component uses a hardcoded key
'async-data-component'foruseAsyncData, meaning all instances of this component share the same async data. Whilst this is intentional for testing deduplication behaviour, it's an anti-pattern in production code.Consider adding a comment to clarify this is test-specific behaviour:
+// Note: using a shared key across all component instances to test deduplication. +// In production, use unique keys such as `async-data-${props.id}`. const { data } = await useAsyncData('async-data-component', async () => { await new Promise(resolve => setTimeout(resolve, 100)) return `Hello from async data!` })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
packages/nuxt/src/app/composables/asyncData.ts(1 hunks)test/basic.test.ts(1 hunks)test/fixtures/basic/app/components/AsyncData.vue(1 hunks)test/fixtures/basic/app/pages/async-data/multiple.vue(1 hunks)test/nuxt/use-async-data.test.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T14:42:56.647Z
Learnt from: Tofandel
Repo: nuxt/nuxt PR: 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:
test/fixtures/basic/app/pages/async-data/multiple.vuetest/nuxt/use-async-data.test.tstest/fixtures/basic/app/components/AsyncData.vuetest/basic.test.ts
🧬 Code graph analysis (1)
test/nuxt/use-async-data.test.ts (1)
packages/nuxt/src/app/composables/asyncData.ts (1)
useAsyncData(194-431)
⏰ 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). (2)
- GitHub Check: build
- GitHub Check: code
🔇 Additional comments (2)
test/fixtures/basic/app/pages/async-data/multiple.vue (1)
1-9: LGTM! Simple test fixture for parallel async data behaviour.The template correctly renders five
AsyncDatacomponents with distinct IDs to test deduplication when multiple components use the same async data key.test/basic.test.ts (1)
3150-3160: LGTM! E2E test confirms deduplication fix works correctly.The test validates that all five component instances receive data (
'Hello from async data!') rather thanundefinedwhen sharing the same async data key during SSR. This directly tests the fix for issue #33737.
|
I have to do some digging. This doesnt feel right. |
|
|
||
| vi.useRealTimers() | ||
| }) | ||
|
|
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.
This test does not detect the bug behaviour. Maybe we still want it 😅
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.
pushed a test that seems to catch it!
| if (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') { | ||
| asyncData.status.value = 'idle' | ||
| return | ||
| return nuxtApp._asyncDataPromises[key] |
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.
maybe we dont need this one
danielroe
left a 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.
thank you ❤️
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: 0
🧹 Nitpick comments (1)
test/nuxt/use-async-data.test.ts (1)
1156-1195: New dedupe synchronisation test looks sound, with minor brittleness concernThe test correctly exercises the
dedupe: 'cancel'path with multiple overlappinguseAsyncDatacalls and verifies that all four returned promises resolve only after the final request completes and that they all observe the final value (data.value === 4). This matches the intent of ensuring that deduped executions share the latest resolving promise rather than earlier, now-cancelled ones.One minor concern is that asserting
promiseFnhas been called exactly 4 times bakes in a specific internal calling pattern; if the implementation later changes (while preserving the public semantics that all deduped callers resolve together with the final value), this expectation could become brittle. If you do not intend to guarantee the exact number of handler invocations, you might relax this to assert the final data value and the shared resolution timing, and drop or soften the exact call-count assertion.Otherwise, the timer usage and assertions look consistent with the rest of the suite.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
test/nuxt/use-async-data.test.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
test/nuxt/use-async-data.test.ts
**/*.{ts,tsx,js,jsx,vue}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx,js,jsx,vue}: Use clear, descriptive variable and function names
Add comments only to explain complex logic or non-obvious implementations
Keep functions focused and manageable (generally under 50 lines), and extract complex logic into separate domain-specific files
Remove code that is not used or needed
Use error handling patterns consistently
Files:
test/nuxt/use-async-data.test.ts
**/*.{test,spec}.{ts,tsx,js}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
test/nuxt/use-async-data.test.ts
🧠 Learnings (1)
📚 Learning: 2025-09-10T14:42:56.647Z
Learnt from: Tofandel
Repo: nuxt/nuxt PR: 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:
test/nuxt/use-async-data.test.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). (17)
- GitHub Check: test-fixtures (windows-latest, built, rspack, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (windows-latest, dev, vite-env-api, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite-env-api, 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-on, js, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite-env-api, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
- GitHub Check: release-pkg-pr-new
- GitHub Check: typecheck (windows-latest, bundler)
- GitHub Check: test-benchmark
- GitHub Check: typecheck (ubuntu-latest, bundler)
- GitHub Check: test-size
- GitHub Check: code
🔗 Linked issue
fixes #33737
📚 Description
My approach is to always return the new deduped promise for
useAsyncDataexecutes, if the promise is rejected from deduplication. This causes all executions to resolve simultaneously because they are the 'same' promise, and I believe this is the intended behaviour. If I want to perform some actions after execution, I want to do so after the new promise has been resolved, rather than when the old one has been deduplicated.