---
title: "Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer() · vuejs.school"
description: "Nuxt 4.4 lets you build your own preconfigured useFetch, moves to Vue Router 5 and adds an announcer for screen readers. Here is what to use first."
canonical_url: "https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer"
meta:
  "og:description": "Nuxt 4.4 lets you build your own preconfigured useFetch, moves to Vue Router 5 and adds an announcer for screen readers. Here is what to use first."
  "og:image": "https://vuejs.school/_og/d/a_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCk%2Cdescription_~TnV4dCA0LjQgbGV0cyB5b3UgYnVpbGQgeW91ciBvd24gcHJlY29uZmlndXJlZCB1c2VGZXRjaCwgbW92ZXMgdG8gVnVlIFJvdXRlciA1IGFuZCBhZGRzIGFuIGFubm91bmNlciBmb3Igc2NyZWVuIHJlYWRlcnMuIEhlcmUgaXMgd2hhdCB0byB1c2UgZmlyc3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtNC1jcmVhdGV1c2VmZXRjaC12dWUtcm91dGVyLTUtYW5kLXVzZWFubm91bmNlciI%2Cs_WDxIB7Y5unBtPqnV.png"
  "og:image:alt": "Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer() · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer() · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCk%2Cdescription_~TnV4dCA0LjQgbGV0cyB5b3UgYnVpbGQgeW91ciBvd24gcHJlY29uZmlndXJlZCB1c2VGZXRjaCwgbW92ZXMgdG8gVnVlIFJvdXRlciA1IGFuZCBhZGRzIGFuIGFubm91bmNlciBmb3Igc2NyZWVuIHJlYWRlcnMuIEhlcmUgaXMgd2hhdCB0byB1c2UgZmlyc3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtNC1jcmVhdGV1c2VmZXRjaC12dWUtcm91dGVyLTUtYW5kLXVzZWFubm91bmNlciI%2Cs_WDxIB7Y5unBtPqnV.png"
  "twitter:image:alt": "Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer() · vuejs.school"
---

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

## What shipped

The [release post](https://nuxt.com/blog/v4-4) lists these changes:

- `createUseFetch()` and `createUseAsyncData()`, factories that return your own version of `useFetch` or `useAsyncData` with default options built in.
- Vue Router 5 replaces Vue Router 4 and drops the separate `unplugin-vue-router` dependency.
- Layouts accept props from `definePageMeta`, and those props are typed from the layout's `defineProps`.
- `useAnnouncer()` and the `<NuxtAnnouncer>` component, for announcing in-page changes to screen readers.
- Route generation moved to a library called unrouting. After a change in development, routes are rebuilt up to 28 times faster when no page was added or removed, and about 15 percent faster when one was.
- An experimental `payloadExtraction: 'client'` mode for routes cached with ISR or SWR, plus an in-memory payload cache for those routes that needs no setting.
- A `refresh` option for `useCookie`, and a reset-to-default option for `useState`.
- Clearer import protection errors with a trace of where a server-only import came from, view transition types, copy-pasteable hints when Vite discovers dependencies late, and `nuxt build --profile`.

## Build a preconfigured fetcher with createUseFetch

Most apps talk to one API. Every call needs the same base URL, and every call to a protected endpoint needs the same `Authorization` header. Until now you either repeated those options on each `useFetch` or wrote a wrapper by hand and lost some of the typing and key handling on the way. `createUseFetch()` gives you the wrapper with nothing lost.

The docs call `createUseFetch` a compiler macro, which means Nuxt rewrites the call while building. It only works as an exported declaration in `app/composables/` (or another folder Nuxt scans). Nuxt then injects the keys that stop the same request running twice, once on the server and again during hydration. This fetcher adds the base URL and a bearer token to every call:

*app/composables/useApi.ts*

```
export const useApi = createUseFetch(() => {
  const config = useRuntimeConfig()
  const token = useCookie<string | null>('auth_token')

  return {
    baseURL: config.public.apiBase,
    onRequest({ options }) {
      if (token.value) {
        options.headers.set('Authorization', `Bearer ${token.value}`)
      }
    },
  }
})
```

- `config.public.apiBase` comes from `runtimeConfig.public.apiBase` in `nuxt.config.ts`, which you can change per environment with the `NUXT_PUBLIC_API_BASE` variable.
- The token lives in a cookie, so it is readable during server-side rendering as well as in the browser, and the server-rendered request carries the header too.
- The header is set inside `onRequest`, so the token is read on every request, including a `refresh()` after the user logs in.

A page then calls `useApi` exactly as it would call `useFetch`, with the same options and the same return value:

*app/pages/account.vue*

```
<script setup lang="ts">
type Profile = { name: string; email: string }

const { data: profile, error } = await useApi<Profile>('/me')
</script>

<template>
  <p v-if="error">Could not load your account.</p>
  <dl v-else-if="profile">
    <dt>Name</dt>
    <dd>{{ profile.name }}</dd>
    <dt>Email</dt>
    <dd>{{ profile.email }}</dd>
  </dl>
</template>
```

`createUseAsyncData()` follows the same pattern for `useAsyncData`.

## Defaults with an object, overrides with a function

The factory accepts two shapes, and they behave differently.

Pass a plain object and its options become defaults. A caller can override any of them. The release post's simplest example is a fetcher that never runs on the server: `export const useClientFetch = createUseFetch({ server: false })`.

Pass a function, as `useApi` does, and the options it returns win over the caller's. The function receives the caller's options, so it can read them before deciding, and it runs inside the caller's setup, which is why `useRuntimeConfig()` and `useCookie()` work there. Options the function does not return, such as `query` or `lazy`, still come from the caller.

## Announce in-page changes with useAnnouncer

Nuxt already had `<NuxtRouteAnnouncer>`, which reads out the page title after a navigation. Many changes happen without a navigation, though: a filtered list updates, a form saves, a search returns nothing. A sighted user sees it. A screen reader user hears nothing unless the page says something. `useAnnouncer()` fills that gap.

First add the component once, in `app.vue`, next to the route announcer. Without it, the messages are never rendered into the page:

*app/app.vue*

```
<template>
  <NuxtAnnouncer />
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

Then call `polite()` for news that can wait until the screen reader finishes its sentence, and `assertive()` for news that should interrupt. This order list fetches through the `useApi` composable from above:

*app/components/OrderList.vue*

```
<script setup lang="ts">
type Order = { id: number; customer: string }

const filter = ref<'open' | 'shipped'>('open')
const { polite, assertive } = useAnnouncer()

const { data: orders, error } = await useApi<Order[]>('/orders', {
  query: { status: filter },
})

watch(orders, (list) => {
  if (list) polite(`${list.length} ${filter.value} orders`)
})

watch(error, (err) => {
  if (err) assertive('Could not load orders. Try again.')
})
</script>

<template>
  <label for="order-filter">Show</label>
  <select id="order-filter" v-model="filter">
    <option value="open">Open orders</option>
    <option value="shipped">Shipped orders</option>
  </select>
  <ul>
    <li v-for="order in orders" :key="order.id">{{ order.customer }}</li>
  </ul>
</template>
```

Changing the select refetches, because `filter` is a ref inside `query`. Focus stays on the select while the list below it changes, which is exactly the case the release post describes: content that changes without a matching focus change.

The release post also says you do not need the announcer everywhere. Moving focus to new content, or native form validation, is often enough.

## Vue Router 5 replaces Vue Router 4

Nuxt 4.4 depends on Vue Router 5. The Vue Router 5.0 release merged `unplugin-vue-router`, the plugin behind file-based routing and typed routes, into the core package. Its [migration guide](https://router.vuejs.org/guide/migration/v4-to-v5.html) says there are no breaking changes for apps that did not use that plugin, and the Nuxt release post expects the upgrade to be transparent for most Nuxt apps.

If you installed `unplugin-vue-router` yourself, remove it from `package.json` and move any imports from it to the `vue-router` paths the migration guide lists. The release post names the next step as taking typed routes out of experimental status.

## Pass typed props to a layout

Layouts can now take props from the page that uses them, which replaces the `provide` and `inject` workarounds people used to parameterise a layout. The page names the layout and its props in `definePageMeta`:

*app/pages/dashboard.vue*

```
<script setup lang="ts">
definePageMeta({
  layout: {
    name: 'panel',
    props: { sidebar: true, title: 'Dashboard' },
  },
})
</script>

<template>
  <p>Welcome back.</p>
</template>
```

The `panel` layout, in `app/layouts/panel.vue`, declares `sidebar` and `title` with `defineProps<{ sidebar?: boolean; title?: string }>()`. Nuxt types the `props` object from that declaration, so a misspelt prop is an error in your editor.

## Reset state, refresh cookies and profile builds

`useState` can now return to its initial value when you clear it. The release post's short example calls `clearNuxtState('counter')` alone, but that resets only with `compatibilityVersion: 5` or the `experimental.defaults.useState.resetOnClear` setting. On a default Nuxt 4.4 config, pass `{ reset: true }`:

*app/composables/useCounter.ts*

```
export function useCounter() {
  const count = useState('counter', () => 0)
  const reset = () => clearNuxtState('counter', { reset: true })
  return { count, reset }
}
```

Without the option, `count.value` becomes `undefined`, as before.

`useCookie` takes `refresh: true`, which extends a cookie's expiry each time you assign it, even when the value is the same. The expiry does not move on its own, so assign the value when the user does something (`session.value = session.value` is enough). That keeps a session cookie alive while the user is active.

`nuxt build --profile` writes three reports: a Chrome trace in `.nuxt/perf-trace.json`, a JSON report in `.nuxt/perf-report.json` and a CPU profile in `nuxt-build.cpuprofile`. Run it once on a slow build to see which module or plugin takes the time.

## Upgrade, and where Vite 8 stands

The release post recommends `npx nuxt upgrade --dedupe`, which also deduplicates your lockfile. Run it, then check what you ended up with:

```
npx nuxt upgrade --dedupe
npm ls nuxt vue-router vite
```

You should see `nuxt` 4.4.2, `vue-router` 5.0 and, for now, `vite` 7.3.

Vite 8.0 came out about two hours after Nuxt 4.4 on the same day, and the release post says nothing about it. The package does: `@nuxt/vite-builder` 4.4.2 depends on `vite` ^7.3.1, so Nuxt 4.4 builds with Vite 7.3. The changelog includes a fix that makes Nuxt's SSR styles plugin compatible with Rolldown, which is groundwork rather than support. Do not force Vite 8 into a Nuxt project with a package manager override; wait for a Nuxt release that lists it.

Upgrade now. Vue Router 5 is a quiet change for most apps, and `createUseFetch()` alone is worth the afternoon it takes to move your API calls into one composable.

**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-4-4-createusefetch-vue-router-5-and-useannouncer#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-03-19","description":"Nuxt 4.4 lets you build your own preconfigured useFetch, moves to Vue Router 5 and adds an announcer for screen readers. Here is what to use first.","name":"Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer()","url":"https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer","breadcrumb":{"@id":"https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer"]}],"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-4-4-createusefetch-vue-router-5-and-useannouncer#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-03-19","description":"Nuxt 4.4 lets you build your own preconfigured useFetch, moves to Vue Router 5 and adds an announcer for screen readers. Here is what to use first.","headline":"Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer()","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCk%2Cdescription_~TnV4dCA0LjQgbGV0cyB5b3UgYnVpbGQgeW91ciBvd24gcHJlY29uZmlndXJlZCB1c2VGZXRjaCwgbW92ZXMgdG8gVnVlIFJvdXRlciA1IGFuZCBhZGRzIGFuIGFubm91bmNlciBmb3Igc2NyZWVuIHJlYWRlcnMuIEhlcmUgaXMgd2hhdCB0byB1c2UgZmlyc3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtNC1jcmVhdGV1c2VmZXRjaC12dWUtcm91dGVyLTUtYW5kLXVzZWFubm91bmNlciI%2Cs_WDxIB7Y5unBtPqnV.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-4-4-createusefetch-vue-router-5-and-useannouncer#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nuxt-4-4-createusefetch-vue-router-5-and-useannouncer#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Nuxt 4.4: createUseFetch(), Vue Router 5 and useAnnouncer()","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_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCk%2Cdescription_~TnV4dCA0LjQgbGV0cyB5b3UgYnVpbGQgeW91ciBvd24gcHJlY29uZmlndXJlZCB1c2VGZXRjaCwgbW92ZXMgdG8gVnVlIFJvdXRlciA1IGFuZCBhZGRzIGFuIGFubm91bmNlciBmb3Igc2NyZWVuIHJlYWRlcnMuIEhlcmUgaXMgd2hhdCB0byB1c2UgZmlyc3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtNC1jcmVhdGV1c2VmZXRjaC12dWUtcm91dGVyLTUtYW5kLXVzZWFubm91bmNlciI%2Cs_WDxIB7Y5unBtPqnV.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCkgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~TnV4dCA0LjQ6IGNyZWF0ZVVzZUZldGNoKCksIFZ1ZSBSb3V0ZXIgNSBhbmQgdXNlQW5ub3VuY2VyKCk%2Cdescription_~TnV4dCA0LjQgbGV0cyB5b3UgYnVpbGQgeW91ciBvd24gcHJlY29uZmlndXJlZCB1c2VGZXRjaCwgbW92ZXMgdG8gVnVlIFJvdXRlciA1IGFuZCBhZGRzIGFuIGFubm91bmNlciBmb3Igc2NyZWVuIHJlYWRlcnMuIEhlcmUgaXMgd2hhdCB0byB1c2UgZmlyc3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9udXh0LTQtNC1jcmVhdGV1c2VmZXRjaC12dWUtcm91dGVyLTUtYW5kLXVzZWFubm91bmNlciI%2Cs_WDxIB7Y5unBtPqnV.png"}]}
```