Discover the completely rewritten data fetching layer in Nuxt 3.17 featuring reactive keys, deduped watch calls, shared refs, granular caching, and improved developer experience. Learn how these performance enhancements make useAsyncData more powerful for your Vue applications.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
One of the biggest features in Nuxt 3.17 is a complete rewrite of the data fetching layer, with significant performance and memory improvements, more consistent behaviour, and a couple new features.
At a glance, here's what's changed:
watch
it will only trigger a single requestuseAsyncData
callsgetCachedData
for better controlWe'll dive into each of these.
You can also check out the demo repo I created here.
The headline feature here is that we get reactive keys, which makes the useAsyncData
composable far more powerful.
(I'm often talking about "reactifying" your composables by adding in as much reactivity as possible.)
Before we could only pass in a string as the key:
const { data } = useAsyncData('my-key', () =>
$fetch('https://some-url')
)
But now, we can pass in any reactive object (as a MaybeRefOrGetter
type):
// Getter (some function)
const { data } = useAsyncData(
() => `key-${index + 1}`,
() => $fetch('https://some-url')
)
// Ref
const myRefKey = ref('my-key')
const { data } = useAsyncData(myRefKey, () => $fetch('https://some-url'))
// Computed
const myComputedKey = computed(() => `my-key-${someOtherRef.value}`)
const { data } = useAsyncData(myComputedKey, () => $fetch('https://some-url'))
This gives us more flexibility in how our fetched data is stored in the payload:
// List of character IDs and names for the dropdown
const characters = [
{ id: 1, name: 'Luke Skywalker' },
{ id: 2, name: 'C-3PO' },
{ id: 3, name: 'R2-D2' },
{ id: 4, name: 'Darth Vader' },
{ id: 5, name: 'Leia Organa' },
]
const selectedId = ref(characters[0].id)
// Reactive key for useAsyncData
const characterKey = computed(
() => `sw-character-${selectedId.value}`
)
const {
data: character,
pending,
error,
refresh,
} = await useAsyncData(characterKey, () =>
$fetch(
`https://swapi.py4e.com/api/people/${selectedId.value}/`
)
)
If you click around a bit, you get a payload that looks like this:
You'll notice that we get to keep all the data we've fetched, because we have a unique key per character.
If we have to use a static string for the character, we don't get that:
// List of character IDs and names for the dropdown
const characters = [
{ id: 1, name: 'Luke Skywalker' },
{ id: 2, name: 'C-3PO' },
{ id: 3, name: 'R2-D2' },
{ id: 4, name: 'Darth Vader' },
{ id: 5, name: 'Leia Organa' },
]
const selectedId = ref(characters[0].id)
const {
data: character,
pending,
error,
refresh,
} = await useAsyncData('sw-character', () =>
$fetch(
`https://swapi.py4e.com/api/people/${selectedId.value}/`
)
)
Every time we load a new character, the new data overwrites the old data, definitely not what we want!
Now, if there are two calls to useAsyncData
for the same key that watch the same source, it will only trigger a single request:
// Component A
const { data: character } = await useAsyncData(
'sw-character',
() =>
$fetch(
`https://swapi.py4e.com/api/people/${selectedId.value}/`
),
{
watch: [selectedId],
}
)
// Component B
const { data: character } = await useAsyncData(
'sw-character',
() =>
$fetch(
`https://swapi.py4e.com/api/people/${selectedId.value}/`
),
{
watch: [selectedId],
}
)
Before, each time you used useAsyncData
or useFetch
, you'd get back an entirely new ref object.
Now, these data fetching composables return the exact same objects, saving tons of memory and improving performance.
For example, if data is updated or mutated like this, it affects all components that are using this key:
function mutateData() {
character.value = {
name: 'Michael Thiessen',
height: 180,
mass: 70,
hair_color: 'blonde',
skin_color: 'fair',
eye_color: 'blue',
birth_year: '1993',
gender: 'male',
}
}
This prevents a lot of bugs around inconsistency as well, and makes it much easier to rely on Nuxt's built-in composables for lightweight state management before reaching for Pinia.
Check out the docs for useAsyncData
here.
In useAsyncData
we can pass in an optional method, getCachedData
, that checks to see if we should fetch new data or rely on what's already in our cache:
const { data: character } = await useAsyncData(
'sw-character',
() =>
$fetch(
`https://swapi.py4e.com/api/people/${selectedId.value}/`
),
{
// Check to see if we already have the data in our payload
getCachedData: (key, nuxtApp) => {
return (
nuxtApp.payload.data[key] ||
nuxtApp.static.data[key]
)
},
}
)
In Nuxt 3.17 we get a new context object passed in, so we have more information on why a fetch is happening:
getCachedData: (key, nuxtApp, context) => {
if (context.cause === 'refresh:manual') {
// Always refetch data when `refresh` is called
return null
}
return (
nuxtApp.payload.data[key] ||
nuxtApp.static.data[key]
)
},
Currently, there are four different reasons a fetch could be triggered:
initial
watch
refresh
— refresh:manual
refresh:hook
Here's the type that gets passed in:
type AsyncDataRequestContext = {
/** The reason for this data request */
cause:
| 'initial'
| 'refresh:manual'
| 'refresh:hook'
| 'watch'
}
This one is disabled by default because it is a breaking change, unless you have v4 compatibility turned on. You can also turn it on through the experimental config flag:
export default defineNuxtConfig({
experimental: {
granularCachedData: true,
},
})
Lastly, we get a few nice DX improvements that are necessary now that calls with the same key are sharing the same underlying object.
Some options conflict because of the way that the data is fetched, and Nuxt will now warn you about these:
handler
deep
transform
pick
getCachedData
default
But there are several options that can be different, because they don't affect the underlying object:
server
lazy
immediate
dedupe
watch
This rewrite is pretty substantial, and you can check out the pull request for the full details.
Here's a full list of the bugs squashed in that PR:
key
had to be a plain string, so any ref/computed key broke caching and refreshNuxtData
logic.pending
and status
stopped syncing between calls with identical keys, making loaders unreliable.getCachedData
when refresh
ing asyncData: getCachedData
was ignored on refresh/watch, causing needless network hits.status: success
, so UI flashed stale data before refetching.