7 New Features in Nuxt 3.9

There’s a lot of new stuff in Nuxt 3.9, and I took some time to dive into a few of them.

In this article I’m going to cover:

  1. Debugging hydration errors in production
  2. The new useRequestHeader composable
  3. Customizing layout fallbacks
  4. Add dependencies to your custom plugins
  5. Fine-grained control over your loading UI
  6. The new callOnce composable — such a useful one!
  7. Deduplicating requests — applies to useFetch and useAsyncData composables

You can read the announcement post here for links to the full release and all PRs that are included. It’s good reading if you want to dive into the code and learn how Nuxt works!

Let’s begin!

1. Debug hydration errors in production Nuxt

Hydration errors are one of the trickiest parts about SSR — especially when they only happen in production.

Thankfully, Vue 3.4 lets us do this.

In Nuxt, all we need to do is update our config:

export default defineNuxtConfig({
  debug: true,
  // rest of your config...

If you aren’t using Nuxt, you can enable this using the new compile-time flag: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__. This is what Nuxt uses.

Enabling flags is different based on what build tool you’re using, but if you’re using Vite this is what it looks like in your vite.config.js file:

import { defineConfig } from 'vite'

export default defineConfig({
  define: {

Turning this on will increase your bundle size, but it’s really useful for tracking down those pesky hydration errors.

2. useRequestHeader

Grabbing a single header from the request couldn’t be easier in Nuxt:

const contentType = useRequestHeader('content-type');

This is super handy in middleware and server routes for checking authentication or any number of things.

If you’re in the browser though, it will return undefined.

This is an abstraction of useRequestHeaders, since there are a lot of times where you need just one header.

See the docs for more info.

3. Nuxt layout fallback

If you’re dealing with a complex web app in Nuxt, you may want to change what the default layout is:

<NuxtLayout fallback="differentDefault">
  <NuxtPage />

Normally, the NuxtLayout component will use the default layout if no other layout is specified — either through definePageMeta, setPageLayout, or directly on the NuxtLayout component itself.

This is great for large apps where you can provide a different default layout for each part of your app.

4. Nuxt plugin dependencies

When writing plugins for Nuxt, you can specify dependencies:

export default defineNuxtPlugin({
  name: 'my-sick-plugin-that-will-change-the-world',
  dependsOn: ['another-plugin']
  async setup (nuxtApp) {
    // The setup is only run once `another-plugin` has been initialized

But why do we need this?

Normally, plugins are initialized sequentially — based on the order they are in the filesystem:

- 01.firstPlugin.ts    // Use numbers to force non-alphabetical order
- 02.anotherPlugin.ts
- thirdPlugin.ts

But we can also have them loaded in parallel, which speeds things up if they don’t depend on each other:

export default defineNuxtPlugin({
  name: 'my-parallel-plugin',
  parallel: true,
  async setup (nuxtApp) {
    // Runs completely independently of all other plugins

However, sometimes we have other plugins that depend on these parallel plugins. By using the dependsOn key, we can let Nuxt know which plugins we need to wait for, even if they’re being run in parallel:

export default defineNuxtPlugin({
  name: 'my-sick-plugin-that-will-change-the-world',
  dependsOn: ['my-parallel-plugin']
  async setup (nuxtApp) {
    // Will wait for `my-parallel-plugin` to finish before initializing

Although useful, you don’t actually need this feature (probably). Pooya Parsa has said this:

I wouldn't personally use this kind of hard dependency graph in plugins. Hooks are much more flexible in terms of dependency definition and pretty sure every situation is solvable with correct patterns. Saying I see it as mainly an "escape hatch" for authors looks good addition considering historically it was always a requested feature.

5. Nuxt Loading API

In Nuxt we can get detailed information on how our page is loading with the useLoadingIndicator composable:

const {
} = useLoadingIndicator();

console.log(`Loaded ${progress.value}%`); // 34%

It’s used internally by the <NuxtLoadingIndicator> component, and can be triggered through the page:loading:start and page:loading:end hooks (if you’re writing a plugin).

But we have lots of control over how the loading indicator operates:

const {
  start,      // Start from 0
  set,        // Overwrite progress
  finish,     // Finish and cleanup
  clear       // Clean up all timers and reset
} = useLoadingIndicator({
  duration: 1000,  // Defaults to 2000
  throttle: 300,   // Defaults to 200

We’re able to specifically set the duration, which is needed so we can calculate the progress as a percentage. The throttle value controls how quickly the progress value will update — useful if you have lots of interactions that you want to smooth out.

The difference between finish and clear is important. While clear resets all internal timers, it doesn’t reset any values.

The finish method is needed for that, and makes for more graceful UX. It sets the progress to 100, isLoading to true, and then waits half a second (500ms). After that, it will reset all values back to their initial state.

6. Nuxt callOnce

If you need to run a piece of code only once, there’s a Nuxt composable for that (since 3.9):

<script setup>
await callOnce(async () => {
  // This will only be run one time, even with SSR

Using callOnce ensures that your code is only executed one time — either on the server during SSR or on the client when the user navigates to a new page.

You can think of this as similar to route middleware — only executed one time per route load. Except callOnce does not return any value, and can be executed anywhere you can place a composable.

It also has a key similar to useFetch or useAsyncData, to make sure that it can keep track of what’s been executed and what hasn’t:

<script setup>
['one', 'two', 'three'].forEach(item => {
    // Run once for each item
    callOnce(item, async () => {
        // Do something with the item

By default Nuxt will use the file and line number to automatically generate a unique key, but this won’t work in all cases.

7. Dedupe fetches in Nuxt

Since 3.9 we can control how Nuxt deduplicates fetches with the dedupe parameter:

useFetch('/api/menuItems', {
  dedupe: 'cancel'  // Cancel the previous request and make a new request

The useFetch composable (and useAsyncData composable) will re-fetch data reactively as their parameters are updated. By default, they’ll cancel the previous request and initiate a new one with the new parameters.

However, you can change this behaviour to instead defer to the existing request — while there is a pending request, no new requests will be made:

useFetch('/api/menuItems', {
  dedupe: 'defer'  // Keep the pending request and don't initiate a new one

This gives us greater control over how our data is loaded and requests are made.

Wrapping Up

If you really want to dive into learning Nuxt — and I mean, really learn it — then Mastering Nuxt 3 is for you.

We cover tips like this, but we focus on the fundamentals of Nuxt.

Starting from routing, building pages, and then going into server routes, authentication, and more. It’s a fully-packed full-stack course and contains everything you need in order to build real-world apps with Nuxt.

Check out Mastering Nuxt 3 here.

Original article written by Michael Theissen