Skip to content

fix(nuxt): use single synced asyncdata instance per key #31373

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

Merged
merged 33 commits into from
Apr 14, 2025

Conversation

danielroe
Copy link
Member

@danielroe danielroe commented Mar 14, 2025

🔗 Linked issue

resolves #21532
resolves #24332 and therefore closes #25850
resolves #22348
resolves #27552
resolves #23522 and therefore closes #23993
resolves #27204
resolves #26733

partly implements #15438

📚 Description

This PR is a major reorganisation of the data fetching layer in Nuxt, providing performance/memory improvements + increased consistency.

⚠️ Breaking Changes (only if v4 compatibility is enabled)

1. getCachedData behavior change

The getCachedData option now:

  • Always gets called before refetching data, even when called by watch or refreshNuxtData
  • It receives a context object with additional information in a cause property
  • Can make more granular decisions about serving cached data
// Before:
useAsyncData('users', fetchUsers, {
  getCachedData: (key, nuxtApp) => {
    return nuxtApp.isHydrating 
      ? nuxtApp.payload.data[key] 
      : nuxtApp.static.data[key]
  }
})

// Now:
useAsyncData('users', fetchUsers, {
  getCachedData: (key, nuxtApp, ctx) => {
    // ctx.cause can be 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
    
    // Example: Don't use cache on manual refresh
    if (ctx.cause === 'refresh:manual') return undefined
    
    return cachedData[key]
  }
})

🐞 Bug Fixes

1. Shared refs for the same key

All calls to useAsyncData or useFetch with the same key will now share not just the underlying data but also data, error, and status refs. This ensures consistency across components but may affect code that expected isolated instances.

// Before: These would be independent instances with separate refs
const { data: users1, status: status1 } = useAsyncData('users', () => $fetch('/api/users'))
const { data: users2, status: status2 } = useAsyncData('users', () => $fetch('/api/users'))

// Now: Both reference the same underlying state
// Any changes to one will affect the other

2. Warnings for inconsistent options

Multiple calls to useAsyncData with the same key but different options will now trigger development warnings. The following options must be consistent across all calls with the same key:

  • handler function
  • deep option
  • transform function
  • pick array
  • getCachedData function
  • default value

The following options can differ without triggering warnings:

  • server
  • lazy
  • immediate
  • dedupe
  • watch
// ❌ This will trigger a warning
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })

// ✅ This is allowed
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })

✨ New Features

1. Reactive keys

You can now use computed refs, plain refs or getter functions as keys, allowing for dynamic data fetching that automatically updates when dependencies change:

// Using a computed property as a key
const userId = ref('123')
const { data: user } = useAsyncData(
  computed(() => `user-${userId.value}`),
  () => fetchUser(userId.value)
)

// When userId changes, the data will be automatically refetched
// and the old data will be cleaned up if no other components use it
userId.value = '456'

2. Deduped watch calls

Multiple components watching the same data source (like route changes) will now trigger only a single data refetch:

// In ComponentA.vue
const { data: users } = useAsyncData(
  'users', 
  () => $fetch(`/api/users?page=${route.query.page}`),
  watch: [() => route.query.page]
)

// In ComponentB.vue
const { data: users } = useAsyncData(
  'users', 
  () => $fetch(`/api/users?page=${route.query.page}`),
  watch: [() => route.query.page]
)

// When route.query.page changes, only one fetch will be performed

🔄 Migration Guide

For getCachedData users

If you were using getCachedData, update your implementation to handle the new context parameter:

// Update your getCachedData implementation
useAsyncData('key', fetchFunction, {
  getCachedData: (key, ctx) => {
    // Handle the context object
    if (ctx.cause === 'refresh:manual') {
      // Skip cache on manual refresh
      return
    }
    
    return yourCacheImplementation.get(key)
  }
})

Alternatively, for now, you can disable this behaviour with:

export default defineNuxtConfig({
  experimental: {
    granularCachedData: false,
    purgeCachedData: false
  }
})

For duplicate key users

If you were intentionally using the same key in multiple places:

  1. Use unique keys if you need independent behavior
  2. Or, ensure consistent options across all calls with the same key (see the list of options that must be consistent above)
  3. Consider extracting your data fetching into a composable to ensure consistency:
// composables/useUserData.ts
export function useUserData(userId) {
  return useAsyncData(
    `user-${userId}`,
    () => fetchUser(userId),
    { 
      deep: true,
      transform: (user) => ({ ...user, lastAccessed: new Date() })
    }
  )
}

// Now use this composable everywhere instead of direct useAsyncData calls

🧪 Testing Recommendations

When updating to this version:

  1. Test components that use the same key in multiple places
  2. Verify that reactive UI updates correctly when using shared keys
  3. Test manual refresh behavior with any custom cache implementations
  4. Check that reactive keys work as expected when their dependencies change
  5. Run your app in development mode to catch any warnings about inconsistent options

🚧 TODO

  • consider caching utils to make it easier for people to migrate from getCachedData

Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Copy link

pkg-pr-new bot commented Mar 14, 2025

Open in StackBlitz

@nuxt/kit

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

nuxt

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

@nuxt/rspack-builder

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

@nuxt/schema

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

@nuxt/vite-builder

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

@nuxt/webpack-builder

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

commit: bfa5a95

Copy link

codspeed-hq bot commented Mar 14, 2025

CodSpeed Performance Report

Merging #31373 will not alter performance

Comparing feat/synced-asyncdata (bfa5a95) with main (31b46e3)

Summary

✅ 10 untouched benchmarks

Copy link
Member

@DamianGlowala DamianGlowala left a comment

Choose a reason for hiding this comment

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

Co-authored-by: Damian Głowala <damian.glowala.rebkow@gmail.com>
@Mini-ghost
Copy link
Member

Mini-ghost commented Mar 17, 2025

After this PR, would dedupe no longer be necessary?

From my testing, when the key is the same, setting dedupe to either cancel or defer does not seem to affect the final displayed result. However, when set to cancel, the API gets triggered twice in succession.

Here is the test code I used:

<script lang="ts">
const fetch = () => {
  return new Promise((resolve) => {
    console.log('fetch...')
    setTimeout(() => {
      resolve('Nuxt Data 1')
    }, 1000)
  })
}

function useMyAsyncData() {
  return useAsyncData('NUXT_DATA', fetch, { 
    dedupe: 'cancel' // or 'defer' 
  })
}
</script>

<script setup lang="ts">
const { data: d1 } = useMyAsyncData()
const { data: d2 } = useMyAsyncData()
</script>

Would it make sense to default dedupe to defer, or perhaps even remove this setting in v4 to prevent potential confusion for users regarding repeated API requests?

@danielroe danielroe marked this pull request as ready for review March 17, 2025 13:31
Copy link

coderabbitai bot commented Mar 17, 2025

Walkthrough

The changes introduce a new experimental configuration option, granularCachedData, which controls whether cached responses from asynchronous data fetching are utilised during refreshes. The function signatures for both useAsyncData and useFetch have been updated to include an additional context parameter, providing detailed information on the cause of data requests. New types such as AsyncDataRequestContext and AsyncDataRefreshCause have been added to support this enhanced functionality. Furthermore, the composables now support reactive keys by allowing computed or plain refs as keys, which facilitates dynamic data fetching and the sharing of state across components. The Nuxt configuration has been extended with a new Vite plugin for enhanced development logging. Documentation and upgrade guides have been updated to clarify these modifications, while tests and fixtures have been revised to reflect the new behaviours, including removing outdated test cases.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65f7225 and bfa5a95.

📒 Files selected for processing (1)
  • packages/nuxt/src/app/composables/fetch.ts (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: code
🔇 Additional comments (5)
packages/nuxt/src/app/composables/fetch.ts (5)

3-3: Import type enhancements for reactive support

The addition of MaybeRefOrGetter from Vue signals improved support for reactive values in the composable API.


38-38: Enhanced flexibility with reactive keys

The type change from string to MaybeRefOrGetter<string> enables dynamic data fetching by allowing computed properties or refs as keys, which aligns with the PR objective of supporting reactive keys.


100-102: Improved key generation with reactive support

The key generation logic has been correctly updated to:

  1. Use computed() to create a reactive key
  2. Handle reactive option keys with toValue()
  3. Properly check the value within the computed property

This change supports the PR goal of ensuring consistency when using the same key across components.


108-108: Reactive key computation

Making the final key computation reactive ensures that when a reactive key changes, cached data is properly invalidated and refetched. This is essential for the dynamic data fetching feature mentioned in the PR objectives.


270-272:

❓ Verification inconclusive

Request body included in cache key

Including the request body hash in the cache key ensures requests with identical URLs but different payloads don't share the same cache entry. This is a necessary enhancement to prevent incorrect data being returned from cache.

However, there's a potential performance consideration when hashing large request bodies. For extremely large payloads, this could impact performance.


🏁 Script executed:

#!/bin/bash
# Check if there are any large request bodies being used in the codebase
rg -A 3 -B 3 "useFetch|fetch\(" --type vue --type js --type ts | grep -i -E "body:|body ="

Length of output: 120


Cache Key Improvement Confirmed – Manual Performance Check Advised

The implementation correctly incorporates a hash of the request body into the cache key to prevent mixing responses from requests with identical URLs but different payloads. Our initial automated search did not flag any usage of large request bodies in JavaScript or TypeScript files. However, due to an issue with recognising Vue file types during the search, please verify manually that no Vue components send exceptionally large payloads which could impact performance during hashing.

  • The changes in packages/nuxt/src/app/composables/fetch.ts (lines 270–272) are approved in principle.
  • A manual review of Vue files is recommended to ensure that hashing large bodies (if any) won’t degrade performance.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 (2)
docs/1.getting-started/18.upgrade.md (1)

273-288: Consider adding code comments for clarity in the diff example

While the diff example for updating getCachedData is good, it would be helpful to add more inline comments explaining what each case of ctx.cause represents and when it might be useful to handle differently.

useAsyncData('key', fetchFunction, {
-  getCachedData: (key, nuxtApp) => {
-    return cachedData[key]
-  }
+  getCachedData: (key, nuxtApp, ctx) => {
+    // ctx.cause - can be 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
+    
+    // Example: Don't use cache on manual refresh
+    if (ctx.cause === 'refresh:manual') return undefined
+    
+    // Example: Use different caching strategy for watch-triggered refreshes
+    if (ctx.cause === 'watch') {
+      // Custom watch caching logic
+    }
+    
+    return cachedData[key]
+  }
})
docs/3.api/2.composables/use-fetch.md (1)

83-83: Spacing issue after the sentence

There's an extra space before the ::warning directive which could affect rendering.

-When using `useFetch` with the same URL and options in multiple components, they will share the same `data`, `error` and `status` refs. This ensures consistency across components. 
+When using `useFetch` with the same URL and options in multiple components, they will share the same `data`, `error` and `status` refs. This ensures consistency across components.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a134e34 and b05d5f9.

📒 Files selected for processing (10)
  • docs/1.getting-started/10.data-fetching.md (3 hunks)
  • docs/1.getting-started/18.upgrade.md (1 hunks)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • docs/3.api/2.composables/use-async-data.md (5 hunks)
  • docs/3.api/2.composables/use-fetch.md (4 hunks)
  • packages/nuxt/src/app/composables/asyncData.ts (17 hunks)
  • packages/nuxt/src/app/composables/fetch.ts (1 hunks)
  • packages/schema/src/config/experimental.ts (1 hunks)
  • test/fixtures/basic-types/types.ts (1 hunks)
  • test/nuxt/composables.test.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/schema/src/config/experimental.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
test/nuxt/composables.test.ts (4)
packages/nuxt/src/app/composables/asyncData.ts (4)
  • useAsyncData (197-389)
  • useNuxtData (493-515)
  • clearNuxtData (530-542)
  • refreshNuxtData (518-527)
packages/nuxt/src/app/composables/index.ts (5)
  • useAsyncData (2-2)
  • useNuxtData (2-2)
  • clearNuxtData (2-2)
  • refreshNuxtData (2-2)
  • useRoute (15-15)
packages/nuxt/src/app/nuxt.ts (1)
  • useNuxtApp (539-552)
packages/nuxt/src/app/composables/router.ts (1)
  • useRoute (20-28)
test/fixtures/basic-types/types.ts (1)
packages/nuxt/src/app/composables/asyncData.ts (2)
  • useAsyncData (197-389)
  • useLazyAsyncData (472-490)
🪛 LanguageTool
docs/1.getting-started/18.upgrade.md

[uncategorized] ~237-~237: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...(Previously, new data was always fetched and this function was not called in these c...

(COMMA_COMPOUND_SENTENCE_2)

docs/1.getting-started/10.data-fetching.md

[uncategorized] ~354-~354: Loose punctuation mark.
Context: ...eAsyncData` will be generated for you. ::tip To get the cached data by key, you ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~358-~358: Loose punctuation mark.
Context: ...docs/api/composables/use-nuxt-data) :: :video-accordion{title="Watch a video fro...

(UNLIKELY_OPENING_PUNCTUATION)

docs/2.guide/3.going-further/1.experimental-features.md

[uncategorized] ~620-~620: Loose punctuation mark.
Context: ... granularCachedData: true } }) ``` ::read-more{icon="i-simple-icons-github" ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~622-~622: Loose punctuation mark.
Context: ...e PR #31373 for implementation details. ::

(UNLIKELY_OPENING_PUNCTUATION)

docs/3.api/2.composables/use-async-data.md

[uncategorized] ~72-~72: Loose punctuation mark.
Context: ...rById(route.params.id) ) </script> ``` ::warning [useAsyncData](/docs/api/comp...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~74-~74: Loose punctuation mark.
Context: ...(/docs/api/composables/use-async-data). :: :read-more{to="/docs/getting-started/...

(UNLIKELY_OPENING_PUNCTUATION)

docs/3.api/2.composables/use-fetch.md

[uncategorized] ~84-~84: Loose punctuation mark.
Context: ...ensures consistency across components. ::warning useFetch is a reserved functi...

(UNLIKELY_OPENING_PUNCTUATION)

🔇 Additional comments (45)
docs/2.guide/3.going-further/1.experimental-features.md (1)

609-623: Well-documented new experimental feature.

The addition of the granularCachedData feature is clearly explained with both its purpose and usage example. This feature provides more control over data fetching behavior by allowing the result from getCachedData to be utilized during refresh operations.

The code example is straightforward and follows the established pattern of other experimental features in this document. The inclusion of a link to the GitHub PR for implementation details provides valuable context for developers who want to understand the inner workings.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~620-~620: Loose punctuation mark.
Context: ... granularCachedData: true } }) ``` ::read-more{icon="i-simple-icons-github" ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~622-~622: Loose punctuation mark.
Context: ...e PR #31373 for implementation details. ::

(UNLIKELY_OPENING_PUNCTUATION)

packages/nuxt/src/app/composables/fetch.ts (2)

100-103: Enhanced key reactivity for dynamic data fetching.

The key computation has been wrapped in a computed() function, making it reactive to changes in its dependencies. This allows for more dynamic behavior when using reactive URL parameters or other changing values. The error handling has also been updated accordingly to check _key.value.

This change is crucial for the reactive keys feature, allowing the composable to respond to changes in the key's dependencies and automatically refetch data when needed.


108-108: Reactive key computation for consistent behavior.

The key variable is now also made reactive through computed(), ensuring it stays in sync with the _key value. This maintains the special prefixing logic ($f) for automatically generated keys while preserving reactivity.

This change ensures that the internal key tracking remains consistent when using reactive keys.

test/fixtures/basic-types/types.ts (2)

548-552: Good type tests for computed key support in useAsyncData.

These tests verify that useAsyncData correctly handles computed properties as keys. The expectTypeOf assertions ensure that the function's return type is properly inferred even when using a computed key instead of a string literal.

This ensures type safety when developers use the new reactive key functionality.


553-556: Type tests for computed key support in useLazyAsyncData.

Similar to the tests for useAsyncData, these assertions verify that useLazyAsyncData properly handles computed property keys with correct type inference.

These tests are important to ensure consistent behavior between the eager and lazy variants of the composable.

docs/1.getting-started/10.data-fetching.md (3)

59-59: Minor grammatical fix.

Corrected possessive apostrophe in "Vue's".


361-398: Clear documentation on shared state and option consistency.

This new section clearly explains how multiple components using the same key will share state, which is crucial information for developers. The documentation helpfully categorizes which options must be consistent across calls with the same key and which can safely differ.

The examples show both incorrect usage that will trigger warnings and correct patterns, providing practical guidance for developers.


400-415: Excellent documentation on reactive keys feature.

This section explains the new ability to use computed refs, plain refs, or getter functions as keys, enabling dynamic data fetching. The example clearly demonstrates how data can be automatically refetched when dependencies change.

This is a powerful addition that makes data fetching more flexible and reactive, aligning with Vue's reactive programming model.

test/nuxt/composables.test.ts (12)

134-139: Well structured test isolation with uniqueKey

Adding a unique key generator for each test ensures proper isolation between test cases and prevents test interference when using the same keys across tests.


141-153: Great helper function for testing async data

The mountWithAsyncData function is a valuable testing utility that encapsulates the complexity of mounting components with async data handling. It returns both the component instance and async data result, making tests more readable and maintainable.


195-206: Confirms expected shared state behavior

This test correctly verifies that multiple calls to useAsyncData with the same key share the same refs, which is a key feature of the new singleton data fetching layer. It also tests the warning that appears when incompatible options are used.


249-288: Comprehensive test for request overriding

This test thoroughly validates the request prioritization behavior when overriding requests. It ensures that:

  1. Initial data from payload is respected
  2. Cancellation works with the dedupe: 'cancel' option
  3. New requests properly update the shared data

The sequence of test steps effectively demonstrates the intended behavior.


302-346: Validates status lifecycle for async requests

Good test coverage of the status transitions (idle → pending → success) throughout the component lifecycle, including unmounting scenarios. This ensures the status ref is accurately maintained.


348-359: Tests cache refresh behavior with context

This test verifies that getCachedData receives the proper context information during refreshes. It confirms the context contains the correct cause value for refresh operations triggered by hooks.


381-386: Verifies cache usage on refresh

Good test to confirm the default behavior of using cache on refresh, ensuring that fetched data respects the cached value returned by getCachedData.


388-410: Validates context parameter in different scenarios

These tests thoroughly check that the getCachedData function receives the correct cause value in different scenarios:

  • Initial fetch
  • Manual refresh
  • Watch-triggered refresh

This ensures the context parameter works correctly in all relevant cases.


455-490: Comprehensive testing of option consistency warnings

Excellent coverage of the warning system for incompatible options. The test validates warnings for different incompatible options (deep, transform, pick, getCachedData, and handler function differences) ensuring users get proper guidance when mixing configurations with the same key.


493-512: Tests for watch deduplicated refreshes

This test confirms that when a watched dependency changes, useAsyncData only refreshes once, preventing redundant API calls. This is crucial for performance when multiple components watch the same data source.


514-547: Validates reactive key support

This test thoroughly checks the reactive key functionality with both ref and getter approaches. It verifies that:

  1. Data is refetched when the key changes
  2. Previous data is properly cleaned up
  3. The payload correctly reflects the current key's data

Great coverage of this important new feature.


549-572: Validates memory cleanup on component unmount

This test ensures that when the last component using a particular key is unmounted, the associated data is cleaned up. This is important to prevent memory leaks in applications with many components and data fetches.

docs/1.getting-started/18.upgrade.md (2)

227-246: Clearly explains the major data fetching changes

This section provides a high-level overview of the significant changes to Nuxt's data fetching system. The impact level classification as "Moderate" is appropriate given the scope of changes.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~237-~237: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...(Previously, new data was always fetched and this function was not called in these c...

(COMMA_COMPOUND_SENTENCE_2)


247-299: Excellent migration guide with clear examples

The migration steps are well-documented with practical examples that illustrate:

  1. How to extract shared data fetching into composables for consistency
  2. How to update getCachedData implementations to handle the new context parameter
  3. Configuration options to disable new behaviors if needed

This provides users with a clear path forward when upgrading.

docs/3.api/2.composables/use-fetch.md (2)

69-84: Well-explained reactive keys and shared state features

The new section clearly explains how to use computed refs or plain refs as dynamic URLs, which is a powerful feature for data fetching that updates automatically with route changes. The shared state explanation is concise and clear.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~84-~84: Loose punctuation mark.
Context: ...ensures consistency across components. ::warning useFetch is a reserved functi...

(UNLIKELY_OPENING_PUNCTUATION)


189-201: Updated signature and added context type

The updated getCachedData signature with the new context parameter aligns with the implementation changes. The AsyncDataRequestContext type is well-documented with clear descriptions of the possible causes.

docs/3.api/2.composables/use-async-data.md (4)

56-72: Clear explanation of reactive keys with example

This section provides a good explanation of how reactive keys work, with a practical example using route parameters. The explanation makes it clear that data will be automatically refetched when the key changes.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~72-~72: Loose punctuation mark.
Context: ...rById(route.params.id) ) </script> ``` ::warning [useAsyncData](/docs/api/comp...

(UNLIKELY_OPENING_PUNCTUATION)


116-144: Comprehensive explanation of shared state rules

This section does an excellent job explaining the shared state behavior and clearly outlines which options must be consistent across calls with the same key and which ones can differ. The examples for both valid and invalid cases provide clear guidance to users.


173-173: Updated function signature to support reactive keys

The function signature now correctly reflects the support for ref and computed ref keys, which is consistent with the new reactive keys feature described in the documentation.


188-194: Updated signature and added context type

The getCachedData function signature has been properly updated with the context parameter, and the AsyncDataRequestContext type is well-documented. This ensures type safety when implementing custom cache strategies based on the fetch cause.

packages/nuxt/src/app/composables/asyncData.ts (17)

44-44: Well-defined type for refresh causes

The new AsyncDataRefreshCause type clearly categorizes the different scenarios that trigger data refreshing, enabling more nuanced caching behavior based on the refresh context.


71-71: Enhanced getCachedData signature with context parameter

The updated signature now includes a context object with the refresh cause, allowing for more intelligent caching decisions based on why the data is being refreshed.


175-175: Support for reactive keys

Converting the key parameter from string to MaybeRefOrGetter<string> enables dynamic data fetching with reactive keys, a significant improvement that allows components to share the same underlying data even when keys are computed.

Also applies to: 193-193, 449-449, 467-467


205-205: Key validation logic for reactive keys

The updated signature handling now correctly supports both string literals and reactive values, with proper type checking that ensures keys are always strings when resolved.

Also applies to: 211-212


224-232: Improved cached data retrieval logic

The default implementation of getCachedData now intelligently handles different refresh causes, only using static data when appropriate (not during manual refreshes or hook-triggered ones).


249-271: Option consistency validation

This new check warns developers when using the same key with inconsistent options or handlers, preventing subtle bugs that could arise from conflicting configurations.


275-276: Shared asyncData instance for same key

Creating a single shared instance for each unique key ensures consistent state across components, addressing the issues mentioned in PR objectives around state synchronization.


278-278: Resource cleanup with dependency tracking

The dependency counter and unregister mechanism ensure proper cleanup of resources when components are unmounted, preventing memory leaks while preserving shared state when needed.

Also applies to: 338-350


332-332: Deduplication of watch execution

Using the shared async data instance for watch callbacks ensures that multiple components watching the same data with the same key won't trigger duplicate API requests, solving one of the key issues mentioned in the PR objectives.


353-364: Key change handling

This watch mechanism elegantly handles reactive key changes by cleaning up old keys and initializing new ones, maintaining proper dependency tracking throughout key changes.


374-382: State synchronization with writableComputedRef

Using computed refs to maintain connections to the shared state ensures that all components using the same key will always see the same data, errors, and status - even if the key changes.


384-386: Promise handling improvements

The promise handling has been improved to ensure that awaiting on asyncData returns the correct references even after key changes, maintaining backward compatibility.


391-400: Utility for writable computed refs

This helper function creates computed refs that maintain a bidirectional connection to the underlying shared state, enabling both reading and writing to the shared data.


577-578: Encapsulated async data creation

The new createAsyncData function cleanly encapsulates all the logic for creating and managing async data instances, separating concerns and making the code more maintainable.

Also applies to: 581-695


619-627: Enhanced caching behavior

The implementation now checks for cached data with appropriate context, supporting the new granularCachedData configuration option mentioned in the summary, which gives more control over when cached responses are used.


683-683: Debounced execution

The debounced execution function prevents excessive calls when multiple components trigger the same data fetch simultaneously, addressing performance concerns mentioned in the PR objectives.


697-704: Hash-based option comparison

The hash function creates consistent identifiers for handlers and options, enabling reliable comparison of configuration to detect inconsistencies when using the same key.

Copy link

@coderabbitai coderabbitai bot left a 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 (2)
docs/2.guide/3.going-further/1.experimental-features.md (1)

609-624: Refine Punctuation and Consistency in "granularCachedData" Section

The new "granularCachedData" section is very informative and clearly explains the feature. However, please review the punctuation around lines 620–622—as static analysis hints suggest there are some loose punctuation marks—to ensure consistent and clear formatting throughout the documentation.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~620-~620: Loose punctuation mark.
Context: ... granularCachedData: true } }) ``` ::read-more{icon="i-simple-icons-github" ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~622-~622: Loose punctuation mark.
Context: ...e PR #31373 for implementation details. ::

(UNLIKELY_OPENING_PUNCTUATION)

docs/1.getting-started/18.upgrade.md (1)

249-256: Clarify Inconsistent Options Migration Step

The migration step addressing potential inconsistencies when using the same key with different options is clearly demonstrated by the example. It might be helpful to emphasise to developers why maintaining consistency in options (such as deep, transform, etc.) is critical, to prevent unexpected warnings.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b05d5f9 and 66ccaeb.

📒 Files selected for processing (3)
  • docs/1.getting-started/18.upgrade.md (1 hunks)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • packages/schema/src/config/experimental.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/schema/src/config/experimental.ts
🧰 Additional context used
🪛 LanguageTool
docs/1.getting-started/18.upgrade.md

[uncategorized] ~238-~238: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...(Previously, new data was always fetched and this function was not called in these c...

(COMMA_COMPOUND_SENTENCE_2)

docs/2.guide/3.going-further/1.experimental-features.md

[uncategorized] ~620-~620: Loose punctuation mark.
Context: ... granularCachedData: true } }) ``` ::read-more{icon="i-simple-icons-github" ...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~622-~622: Loose punctuation mark.
Context: ...e PR #31373 for implementation details. ::

(UNLIKELY_OPENING_PUNCTUATION)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: release-pr
  • GitHub Check: codeql (javascript-typescript)
🔇 Additional comments (9)
docs/1.getting-started/18.upgrade.md (9)

228-242: Singleton Data Fetching Layer: Overview and Impact

The newly introduced "Singleton Data Fetching Layer" section effectively outlines the key changes, including shared refs for identical keys, improved control over the caching function via an updated signature, reactive key support, and automatic data cleanup. This provides users with a solid understanding of the performance and consistency improvements.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~238-~238: Use a comma before “and” if it connects two independent clauses (unless they are closely connected and short).
Context: ...(Previously, new data was always fetched and this function was not called in these c...

(COMMA_COMPOUND_SENTENCE_2)


273-288: Update to getCachedData Signature

The diff showing the update of the getCachedData function signature—adding the new ctx parameter and checking for a manual refresh via ctx.cause—is clear and precise. This change provides a more granular caching strategy, and the inline comment enhances understanding.


290-299: Alternative Configuration for Caching Behaviour

Providing an alternative configuration snippet to disable the new caching behaviour (via granularCachedData and purgeCachedData) is very useful for users who may prefer previous behaviours. The instructions make it clear how to revert the changes if necessary.


320-325: Update Route Metadata Access

The diff example that changes the access from route.meta.name to route.name succinctly addresses the modifications in how route metadata is exposed. This simplification is in line with the updated Nuxt architecture and should reduce ambiguity.


477-487: Simplify Handling of error.data

Replacing manual JSON parsing of error.data with direct assignment clarifies the intent and reduces potential parsing errors. This improvement also aligns with the updated error handling strategy, where the error object now provides pre-parsed data.


669-678: Replace Boolean Values for dedupe Option

The diff replacing boolean values with explicit string values ('cancel' and 'defer') for the dedupe option improves clarity significantly. This change removes the ambiguity associated with using true/false values and makes the API behaviour more predictable.


769-776: Ensure Absolute Watch Paths for Builder Hooks

The added logic using relative and resolve from Node’s fs module ensures that paths emitted in the builder:watch hook are absolute with respect to the project’s source directory. This update enhances compatibility with layered architectures and complex projects.


797-799: Transition from Global window.__NUXT__ Access

Switching from using the global window.__NUXT__ object to accessing Nuxt payload via useNuxtApp().payload is a commendable update. This move promotes better separation of concerns and supports multi-app patterns, aligning with modern Nuxt practices.


860-867: Revise Template Compilation with getContents

The refactoring of template compilation—moving from a static src reference to a dynamic getContents function that utilises a template utility from es-toolkit/compat—is a robust improvement. This approach not only enhances flexibility but also addresses security concerns associated with using lodash’s template function at build time.

@adamdehaven
Copy link
Contributor

adamdehaven commented Apr 28, 2025

@danielroe I'm pulling in the 3.17.0 release into my app and am noticing separate tests utilizing renderSuspended are now failing as the data appears to persist between tests (e.g. utilizing a computed fetchKey that is based on the route.path in the component) as long as the route for each test is the same.

  • Is this expected? Previously the data was seemingly cleared between tests
  • Is there a regular pattern for clearing Nuxt data between tests I should be using?

Example

const fetchKey = computed((): string => `page-${route.path.replace(/\//g, '-')}`)
const { data } = await useFetch('/api/v3/pages/my-page', {
  key: fetchKey,
})

@danielroe
Copy link
Member Author

@adamdehaven this is expected (from a Nuxt point of view) as the fetch instance is global and never unmounted/cleaned up. You can wrap it in a composable. Or call clearNuxtData after/before each test. Or maybe we can look at doing that automatically in @nuxt/test-utils. What do you think?

@adamdehaven
Copy link
Contributor

Or call clearNuxtData after/before each test. Or maybe we can look at doing that automatically in @nuxt/test-utils. What do you think?

I actually tried calling clearNuxtData in a before/after hook and it had zero impact 😬 -- Yes, I think doing this automatically in @nuxt/test-utils would be ideal to totally isolate tests

@metkm
Copy link

metkm commented Apr 29, 2025

@danielroe Shouldn't this feature also apply to useNuxtData? It looks like we can't give reactive key to useNuxtData yet?

@samirmhsnv
Copy link

@danielroe I believe there is a breaking change regarding FormData handling in fetch.js due to the newly added statement:

It originates from ohash. That library cannot hash File objects.

if (opts.body) {
  segments.push(hash(toValue(opts.body)))
}

Reference:
https://github.com/nuxt/nuxt/pull/31373/files#diff-3ace8ffd6a6f0f4eb7979e99189c489e15fa917b74b48c26ee7e12ad13dfb866R270-R272

When I attempt to upload a FormData containing a File in the body, I encounter the following error:
"Cannot serialize File"

const formData = new FormData();

formData.append("image", new_avatar.value);

await useAPI('/company', {
  method: 'POST',
  body: formData,
})

My current solution is to downgrade v3.16.2

@Tofandel
Copy link
Contributor

Tofandel commented May 8, 2025

There seems to be a breaking change in there as well, on route change without remounting components (via <NuxtPage :page-key="(route: RouteLocationResolved) =>(route.matched[0].name || route.path)"></NuxtPage>) then all the previously fetched data is cleared. This was not the case before

I'm trying to dig what's causing this

Okay so after digging, this is entirely due to the key now being computed based on the params
https://github.com/nuxt/nuxt/pull/31373/files#diff-3ace8ffd6a6f0f4eb7979e99189c489e15fa917b74b48c26ee7e12ad13dfb866R100

This means that if any of the params change the key changes and it switches to another data. But that means there is no data until it's fetched again while before what it would do is keep the previous data but fetch the new one while the previous data was available, this is a problem when you are expecting to always have data via top level await in setup components

A workaround for now seems to be to hardcode a static key

@GerryWilko
Copy link

@danielroe Thank you so much for your work!

I'm a little confused by some of the changes to useAsyncData here so I thought I would add my thoughts here and hopefully you can clarify when you get the time.

When initially reading about this upgrade to useAsyncData I initially thought this would resolve the typical issues we would get where using useAsyncData in multiple places with the same key could cause multiple triggers of the handler to be fired and therefore fire unnecessary requests.

This would typically lead me to the discussions around the fact that useAsyncData does not cache data and instead caching needs to be handled independently however we previously couldn't stop the handler from firing on initial call of useAsyncData even if the key provided matched an existing useAsyncData call.

This is where I presumed these changes came in attempting to unify the instance returned by useAsyncData and therefore allowing for the getCachedData function to be respected on subsequent useAsyncData calls.

However from my testing I'm struggling to understand how this works I have a simple unit test here that illustrates the problem:

import type { NuxtApp } from '#app'
import type { AsyncDataRefreshCause } from '#app/composables/asyncData'
import { expect, it, vi } from 'vitest'

const handler = vi.fn(async () => Promise.resolve('hello'))

const getCachedData = vi.fn((key: string, nuxtApp: NuxtApp, ctx: { cause: AsyncDataRefreshCause }) => {
  if (nuxtApp.isHydrating) {
    return nuxtApp.payload.data[key]
  }

  const { data } = useNuxtData(key)
  if (ctx.cause !== 'refresh:manual' && ctx.cause !== 'refresh:hook' && data.value) {
    return data.value
  }
})

// eslint-disable-next-line ts/promise-function-async
function testAsyncData() {
  return useAsyncData('test-key', handler, {
    getCachedData,
  })
}

it('test duplicate calls are not made after first call has finished', async () => {
  const { status, data } = await testAsyncData()
  expect(status.value).toBe('success') // pass
  expect(data.value).toBe('hello') // pass
  expect(handler).toHaveBeenCalledTimes(1) // pass
  const { status: status2, data: data2 } = testAsyncData()
  expect.soft(handler).toHaveBeenCalledTimes(1) // fail - called twice
  expect.soft(getCachedData).toHaveBeenCalledTimes(2)
  expect.soft(data.value).toBe('hello') // pass
  expect.soft(data2.value).toBe('hello') // pass
  expect.soft(status.value).toBe('success') // fail - value is 'pending'
  expect.soft(status2.value).toBe('success') // fail - value is 'pending'
})

Effectively what I am trying to do here is to adjust the default getCachedData but instead always respect the data we have unless you explicitly try to refresh it.

However what actually happens is that whilst the getCachedData is called twice with cause initial the second call is effectively discarded as it is passed only to new useAsyncData instances as the initialCachedData.

// Avoid fetching same key that is already fetched
if (granularCachedData || opts.cause === 'initial' || nuxtApp.isHydrating) {
const cachedData = opts.cause === 'initial' ? initialCachedData : options.getCachedData!(key, nuxtApp, { cause: opts.cause ?? 'refresh:manual' })
if (typeof cachedData !== 'undefined') {
nuxtApp.payload.data[key] = asyncData.data.value = cachedData
asyncData.error.value = asyncDataDefaults.errorValue
asyncData.status.value = 'success'
return Promise.resolve(cachedData)
}
}

It is then used here but it only ever uses the initialCachedData value even though the most recent evaluation of getCachedData returned a value. I presume there are some security considerations here that is the concern? Could this perhaps be adjusted to be that on client we always respect the current cached value (I think this should always be safe on client)?

At present it seems to be that we cannot stop the handler from being triggered on each call to useAsyncData (I believe some deduping occurs whilst the handler is in flight).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment