---
title: "Nuxt 3.17: the async data layer, reworked · vuejs.school"
description: "Nuxt 3.17 makes every useAsyncData call with the same key share one set of refs, accepts reactive keys and adds NuxtTime. What changed, and how to upgrade."
canonical_url: "https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked"
meta:
  "og:description": "Nuxt 3.17 makes every useAsyncData call with the same key share one set of refs, accepts reactive keys and adds NuxtTime. What changed, and how to upgrade."
  "og:image": "https://vuejs.school/_og/d/a_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQ%2Cdescription_~TnV4dCAzLjE3IG1ha2VzIGV2ZXJ5IHVzZUFzeW5jRGF0YSBjYWxsIHdpdGggdGhlIHNhbWUga2V5IHNoYXJlIG9uZSBzZXQgb2YgcmVmcywgYWNjZXB0cyByZWFjdGl2ZSBrZXlzIGFuZCBhZGRzIE51eHRUaW1lLiBXaGF0IGNoYW5nZWQsIGFuZCBob3cgdG8gdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTMtMTctdGhlLWFzeW5jLWRhdGEtbGF5ZXItcmV3b3JrZWQi%2Cs_pyRhkyNzvB427bVe.png"
  "og:image:alt": "Nuxt 3.17: the async data layer, reworked · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Nuxt 3.17: the async data layer, reworked · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQ%2Cdescription_~TnV4dCAzLjE3IG1ha2VzIGV2ZXJ5IHVzZUFzeW5jRGF0YSBjYWxsIHdpdGggdGhlIHNhbWUga2V5IHNoYXJlIG9uZSBzZXQgb2YgcmVmcywgYWNjZXB0cyByZWFjdGl2ZSBrZXlzIGFuZCBhZGRzIE51eHRUaW1lLiBXaGF0IGNoYW5nZWQsIGFuZCBob3cgdG8gdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTMtMTctdGhlLWFzeW5jLWRhdGEtbGF5ZXItcmV3b3JrZWQi%2Cs_pyRhkyNzvB427bVe.png"
  "twitter:image:alt": "Nuxt 3.17: the async data layer, reworked · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What shipped

From the release post, grouped the way it groups them:

- Data fetching: one shared instance per key, reactive keys, and deduplicated refetches when several components watch the same source. The release post says the breaking changes sit behind `experimental.granularCachedData`, which is off by default.
- Components: a new `<NuxtTime>` for rendering dates without hydration mismatches, `<NuxtErrorBoundary>` rewritten as a single-file component, and new props on `<NuxtLink>` and `<NuxtLoadingIndicator>`.
- Developer experience: a warning when a server component has no single root element; a warning when you use the `runtimeConfig.app` namespace, which Nuxt reserves for itself; a warning when a core auto-import preset is overridden; and an error when `definePageMeta` appears twice in one file, which can happen mid-refactor.
- Module authors: `experimental.enforceModuleCompatibility` makes Nuxt stop when an incompatible module loads (off by default; the release post says it will be on by default in the next major version), and `addComponentExports` registers every named export of a file as a component.
- Performance: file globbing moved to `tinyglobby`, the `.data` directory is excluded from type-checking, and the `purgeCachedData` check was hoisted so it tree-shakes better.
- Documentation: the raw Markdown and YAML of the docs is published on npm as `@nuxt/docs`.

## Calls with the same key share one set of refs

The key is the first argument to `useAsyncData` (`'users'` below), the name Nuxt stores the result under. Before 3.17, each call built its own instance around that key's refs, with its own `refresh` and watchers. Two plain calls with one key did share their refs, but not always: a second call with `immediate: false`, for example, made new ones, and the first component never saw what the second fetched.

From 3.17, all calls with the same key, from `useAsyncData` or `useFetch`, share one instance: the same `data`, `error` and `status` refs, and one `refresh`. Refresh in one place and every component using the key updates, and `refreshNuxtData('users')` runs the handler once rather than once per call.

Here are two components that use the `users` key:

*components/UserCount.vue*

```
<script setup lang="ts">
const { data: users } = useAsyncData('users', () => $fetch('/api/users'))
</script>

<template>
  <p>{{ users?.length ?? 0 }} users</p>
</template>
```

*components/UserList.vue*

```
<script setup lang="ts">
const { data: users, status, refresh } = useAsyncData('users', () => $fetch('/api/users'))
</script>

<template>
  <button type="button" :disabled="status === 'pending'" @click="refresh()">Reload</button>
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>
```

Put both on one page. Clicking "Reload" in the list updates the count too, because both components read the same refs.

Sharing an instance means the calls have to agree on how the data is produced. The [pull request](https://github.com/nuxt/nuxt/pull/31373) behind the change splits the options in two:

- **Must match** across calls with one key: `handler`, `deep`, `transform`, `pick`, `getCachedData` and `default`. If they differ, Nuxt prints a development warning that starts `[nuxt] [useAsyncData] Incompatible options detected for "users"` and lists the differences.
- **May differ**, because they describe when and where to fetch: `server`, `lazy`, `immediate`, `dedupe` and `watch`.

So if a key appears in more than one place, move the call into a composable so there is one definition:

*composables/useUsers.ts*

```
export function useUsers() {
  return useAsyncData('users', () => $fetch('/api/users'))
}
```

## Reactive keys follow a changing prop or ref

The key can now be a computed ref, a plain ref or a getter function. When it changes, Nuxt fetches again under the new key and, if no other component still uses the old key, cleans up the old entry. This matters most in a component that stays mounted while its input changes, such as a card that shows whichever user is selected:

*components/UserCard.vue*

```
<script setup lang="ts">
const props = defineProps<{ userId: string }>()

const { data: user, status } = useAsyncData(
  () => `user-${props.userId}`,
  () => $fetch(`/api/users/${props.userId}`),
)
</script>

<template>
  <p v-if="status === 'pending'">Loading</p>
  <h2 v-else-if="user">{{ user.name }}</h2>
</template>
```

A parent renders `<UserCard :user-id="selectedId" />` and changes `selectedId` when the visitor picks another user. The card stays mounted, its key changes from `user-1` to `user-2`, and the fetch follows. Before 3.17 the key had to be a plain string, read once when the component was set up. To refetch when the prop changed you added a `watch` option, and every user was stored under that first key. A reactive key gives each user an entry of its own.

Pages are different. By default `<NuxtPage>` mounts a fresh page component when a route parameter changes, so going from `/users/1` to `/users/2` runs the setup of `pages/users/[id].vue` again, and a string key built from `route.params.id` was already right before 3.17.

## Watchers on one key are deduplicated

The `watch` option takes an array of reactive sources and refetches when any of them changes. Several components on one page often watch the same source: a paginated list, a page counter and a "showing 1 to 20" label might all follow `() => route.query.page`. In 3.16 each of them refetched. In 3.17, when they share a key, the release post says a change to the source triggers a single fetch instead of one per component. Nothing in your code changes to get this; the call in each component looks as it always has:

```
const route = useRoute()

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

## The granularCachedData and purgeCachedData flags

Two `experimental` options control the parts of the rework that could change what your app does. Both concern the cache behind `getCachedData`, the option that returns stored data so Nuxt can skip a request.

`experimental.granularCachedData` is off by default (on if you set `future.compatibilityVersion: 4` to try the next major version's defaults). The [config reference](https://nuxt.com/docs/api/nuxt-config) describes it as "whether to call and use the result from `getCachedData` on manual refresh for `useAsyncData` and `useFetch`".

- Off: Nuxt calls `getCachedData` only on the initial fetch and during hydration, which is what 3.16 did.
- On: Nuxt also calls it before every refetch, including those caused by `watch` and `refreshNuxtData`.

Either way, `getCachedData` now receives a third argument whose `cause` says why it was called: `initial`, `refresh:hook` (from `refreshNuxtData`), `refresh:manual` (from `refresh()`) or `watch`.

`experimental.purgeCachedData` is on by default, described as "whether to clean up Nuxt static and asyncData caches on route navigation". In 3.17 that includes clearing a key's data when the last component using it unmounts. The release post says that if your app relied on cached data staying available after those components unmounted, you can turn it off to get the previous behaviour:

*nuxt.config.ts*

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

Set it only if the upgrade breaks something, and treat the line as a note to fix the code that depended on a stale cache. Data a page needs after navigating away belongs in a store or in a fresh fetch, not in a cache entry that happens to survive.

## NuxtTime and updates to three built-in components

Rendering a date is a classic hydration problem: the server formats it in one time zone and the browser in another, and Vue warns that the text does not match. The release post calls `<NuxtTime>` an SSR-safe time display that resolves these mismatches:

```
<NuxtTime :datetime="post.publishedAt" year="numeric" month="long" day="numeric" />
```

It renders a `<time>` element, and the extra attributes are the usual `Intl.DateTimeFormat` options. On the server it writes those options into `data-` attributes, and a small script formats the date again in the browser before Vue hydrates, so both sides render the same text.

Three other components gained props or exposed values:

- `<NuxtErrorBoundary>` exposes `error` and `clearError` in its error slot types and through `useTemplateRef`, so a parent can reset it.
- `<NuxtLink to="/about" trailing-slash>` renders `href="/about/"`, for sites whose server wants a trailing slash.
- `<NuxtLoadingIndicator :hide-delay="500" :reset-delay="300" />` controls how long the bar waits before hiding and before resetting its state.

## How to upgrade

Nuxt recommends one command, which also refreshes the lockfile so the packages underneath Nuxt move together:

```
npx nuxi@latest upgrade --dedupe
```

Then run the app in development and click through every page that fetches data. Three things to look for:

1. An `Incompatible options detected` warning: two calls with the same key and a different handler, `transform`, `pick` or `default`. Merge them into one composable.
2. Code that reads a key's data after every component using it has unmounted, for example with `useNuxtData`. With `purgeCachedData` on, the data is gone. Fetch it again or keep it in a store.
3. A `watch` option combined with a static key where you wanted separate cache entries. Switch to a reactive key.

Upgrade now if several components share keys in your app, because shared refs keep them in step, and reactive keys replace the `watch` workaround in components whose input changes. Wait a week or two if you depend on `getCachedData` for an offline-first flow: the purge clears `nuxtApp.payload.data[key]`, which a custom `getCachedData` may read. Read the pull request first and test it against your cache logic.

**Check it stuck**3 questions from the Data fetching quiz.

[**Practice now**](https://vuejs.school/learn/nuxt/data-fetching?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**What's new in Nuxt 4.6: nuxt/server, typed $fetch, cookie sessions and Vapor**](https://vuejs.school/community/blog/whats-new-in-nuxt-4-6) [Tutorial · 6 min read**useFetch or $fetch? Picking the right one in Nuxt**](https://vuejs.school/community/blog/usefetch-or-fetch-picking-the-right-one-in-nuxt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-29","description":"Nuxt 3.17 makes every useAsyncData call with the same key share one set of refs, accepts reactive keys and adds NuxtTime. What changed, and how to upgrade.","name":"Nuxt 3.17: the async data layer, reworked","url":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked","breadcrumb":{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-04-29","description":"Nuxt 3.17 makes every useAsyncData call with the same key share one set of refs, accepts reactive keys and adds NuxtTime. What changed, and how to upgrade.","headline":"Nuxt 3.17: the async data layer, reworked","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQ%2Cdescription_~TnV4dCAzLjE3IG1ha2VzIGV2ZXJ5IHVzZUFzeW5jRGF0YSBjYWxsIHdpdGggdGhlIHNhbWUga2V5IHNoYXJlIG9uZSBzZXQgb2YgcmVmcywgYWNjZXB0cyByZWFjdGl2ZSBrZXlzIGFuZCBhZGRzIE51eHRUaW1lLiBXaGF0IGNoYW5nZWQsIGFuZCBob3cgdG8gdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTMtMTctdGhlLWFzeW5jLWRhdGEtbGF5ZXItcmV3b3JrZWQi%2Cs_pyRhkyNzvB427bVe.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nuxt-3-17-the-async-data-layer-reworked#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Nuxt 3.17: the async data layer, reworked","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQ%2Cdescription_~TnV4dCAzLjE3IG1ha2VzIGV2ZXJ5IHVzZUFzeW5jRGF0YSBjYWxsIHdpdGggdGhlIHNhbWUga2V5IHNoYXJlIG9uZSBzZXQgb2YgcmVmcywgYWNjZXB0cyByZWFjdGl2ZSBrZXlzIGFuZCBhZGRzIE51eHRUaW1lLiBXaGF0IGNoYW5nZWQsIGFuZCBob3cgdG8gdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTMtMTctdGhlLWFzeW5jLWRhdGEtbGF5ZXItcmV3b3JrZWQi%2Cs_pyRhkyNzvB427bVe.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCAzLjE3OiB0aGUgYXN5bmMgZGF0YSBsYXllciwgcmV3b3JrZWQ%2Cdescription_~TnV4dCAzLjE3IG1ha2VzIGV2ZXJ5IHVzZUFzeW5jRGF0YSBjYWxsIHdpdGggdGhlIHNhbWUga2V5IHNoYXJlIG9uZSBzZXQgb2YgcmVmcywgYWNjZXB0cyByZWFjdGl2ZSBrZXlzIGFuZCBhZGRzIE51eHRUaW1lLiBXaGF0IGNoYW5nZWQsIGFuZCBob3cgdG8gdXBncmFkZS4%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTMtMTctdGhlLWFzeW5jLWRhdGEtbGF5ZXItcmV3b3JrZWQi%2Cs_pyRhkyNzvB427bVe.png"}]}
```