iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🕌

New Features of Nuxt 3

に公開

Currently, Nuxt.js version 3 is available as a public beta. Many new features have been added in the transition from Nuxt.js 2 to 3.

  • Performance improvements
  • Nitro engine
  • Composition API
  • Vite
  • Vue 3
  • Webpack 5
  • Nuxt CLI
  • Native TypeScript support
  • ESM support
  • Nuxt devtool (not yet)
  • and more...

In addition to the above, all the newly added features are very interesting, and there are many things you'll want to use right away. Let's experience the new features added in Nuxt3!

Installation

From Nuxt3 onwards, you use the new Nuxt CLI instead of create-nuxt-app to create a project.

npx nuxi init nuxt3-app

Checking the generated folder, you can see that it uses TypeScript from the start ✨
The folder structure is much more minimal compared to the Nuxt2 days, where everything was prepared in advance.

.
├── .gitignore
├── README.md
├── app.vue
├── nuxt.config.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Install the packages using npm or yarn.

cd nuxt3-app
npm install 

Once the installation is complete, start it with the following command and access http://localhost:3000. The startup is incredibly fast.

npm run dev

Screenshot 2021-12-12 15.15.42

Volar

From Vue3, it is recommended to use Volar as a VSCode extension instead of Vetur. Make sure to install it. (If Vetur is already installed, you need to disable it. It's a bit of a hassle to switch between Vue2 and Vue3 projects!)

https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar

Enabling TypeScript Strict type checks

While it's great that TypeScript is prepared from the start, for some reason, Strict type checks (strict mode) is not enabled. For example, the following code should trigger a noImplicitAny error if Strict type checks were enabled, but with the current settings, it does not.

<script setup lang="ts">
const fn = (args) => {
  console.log(args);
};
</script>

In a typical TypeScript project, you change the TypeScript settings in tsconfig.json, but looking at the automatically generated tsconfig.json, it looks like this:

{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json"
}

It is simply a file that inherits the settings from ./.nuxt/tsconfig.json. It seems ./.nuxt/tsconfig.json contains the recommended TypeScript settings.

To change these settings, you need to modify the nuxt.config.ts file.

import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
  typescript: {
    strict: true
  }
})

Since you edited nuxt.config.ts, you need to restart the development server.

npm run dev

Now the noImplicitAny error is displayed in the editor... however, unfortunately, it doesn't perform type checking when the development server is started.

Type checking must be executed manually using the npx nuxi typecheck command.

npx nuxi typecheck
Nuxt CLI v3.0.0-27319101.3e82f0f                                                                    15:52:09
npx: installed 93 packages in 29.52s.
pages/index.vue:2:13 - error TS7006: Parameter 'args' implicitly has an 'any' type.

2 const fn = (args) => {
              ~~~~

Found 1 error.

 ERROR  Command failed with exit code 2: npx -p vue-tsc -p typescript vue-tsc --noEmit              15:53:05

  at makeError (node_modules/nuxi/dist/chunks/index3.mjs:1096:11)
  at handlePromise (node_modules/nuxi/dist/chunks/index3.mjs:1660:26)
  at processTicksAndRejections (internal/process/task_queues.js:95:5)
  at async Object.invoke (node_modules/nuxi/dist/chunks/typecheck.mjs:42:7)
  at async _main (node_modules/nuxi/dist/chunks/index.mjs:382:7)

app.vue

As you can tell from the fact that it's not present in the automatically generated folders, the pages directory is no longer mandatory.

When the pages directory is not used, Nuxt does not include vue-router in the dependencies during the build, allowing for a reduction in the bundle file size.

In cases where the pages directory is not used, the app.vue file is used to display the top page.

If you use both app.vue and the pages directory, you can display the current page by placing the <NuxtPage> component within app.vue.

  • app.vue
<template>
  <div>
    <NuxtPage />
  </div>
</template>

If you delete the app.vue file, you can continue to use only the pages directory as before.

Meta Tags

Meta components

Up until Nuxt 2, the head method was used to set the <title> tag and <meta> tags for each page.

<template>
  <h1>{{ title }}</h1>
</template>

<script>
  export default {
    data() {
      return {
        title: 'Hello World!'
      }
    },
    head() {
      return {
        title: this.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'My custom description'
          }
        ]
      }
    }
  }
</script>

Nuxt 3 provides the following components, similar to Next.js's pages/_document.js, allowing you to set <title> tags in a declarative way.

  • <Title>
  • <Base>
  • <Script>
  • <Style>
  • <Meta>
  • <Link>
  • <Body>
  • <Html>
  • <Head>

Note that these components must all start with a capital letter to distinguish them from regular HTML tags. These components can be used as follows:

<script setup lang="ts">
const title = "Hello Nuxt3!!";
</script>

<template>
  <Html lang="ja">
    <Head>
      <Title>{{ title }}</Title>
      <Meta name="description" :content="`This is ${title} page`" />
    </Head>
  </Html>

  <h1>{{ title }}</h1>
</template>

You can see that the <head> tag has been set as expected.

Screenshot 2021-12-12 16.28.09

useMeta

In addition to Meta components, you can also use the useMeta function within the setup function.

<script setup lang="ts">
const title = "Hello Nuxt3!!";

useMeta({
  meta: [{ name: "description", content: `This is ${title} page` }],
});
</script>

<template>
  <h1>{{ title }}</h1>
</template>

Server directory

API Routes

In Nuxt 2, you could use serverMiddleware to create a simple API server without having to use an external server.

By using Nuxt 3's API Routes, you can create an API server similar to the API Routes in Next.js.

API Routes must be created under the ~/server/api directory.
Each file placed under ~/server/api must default export a function that receives req and res.

Let's create the simplest possible echo server as follows.

  • server/api/hello.ts
export default (req, res) => 'Hello Nuxt3 from server!'

Files under the ~/server/api directory use file-system-based routing, just like the pages directory. Try accessing http://localhost:3000/api/hello.

Screenshot 2021-12-12 16.37.54

Let's look at a slightly more interesting example. We'll create an API server that fetches data from a test API called JSON Placeholder and returns it.

First, create the server/api/users.ts file and assign appropriate types to req and res.

import type { IncomingMessage, ServerResponse } from 'http'

export default (req: IncomingMessage, res: ServerResponse) => {

}

You can use the $fetch method anywhere in Nuxt. It uses ohmyfetch, allowing you to use the fetch method without distinguishing between whether it's running on the server or in the browser.

import type { IncomingMessage, ServerResponse } from 'http'

export interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: Address;
  phone: string;
  website: string;
  company: Company;
}
export interface Address {
  street: string;
  suite: string;
  city: string;
  zipcode: string;
  geo: Geo;
}
export interface Geo {
  lat: string;
  lng: string;
}
export interface Company {
  name: string;
  catchPhrase: string;
  bs: string;
}

export default async (req: IncomingMessage, res: ServerResponse) => {
  const result: User[] = await $fetch('https://jsonplaceholder.typicode.com/users')

  return result
}

If you access http://localhost:3000/api/users, you can see that the data is being fetched correctly.

Screenshot 2021-12-12 16.58.10

Server middleware

Files created under ~/server/middleware are treated as server middleware. Server middleware is executed for every request. For example, it can be used for authentication processing or for collecting logs on the middleware.

Just like API Routes, you default export a function that receives req and res.

  • server/middleware/logging.ts
import type { IncomingMessage, ServerResponse } from 'http'

export default async (req: IncomingMessage, res: ServerResponse) => {
  console.log(req.headers)
}

useFetch/useAsyncData

In Nuxt 3, you can use useFetch, useLazyFetch, useAsyncData, and useLazyAsyncData to fetch data from the server.

The difference for functions with Lazy in their name is whether they block page rendering during data retrieval. useFetch and useAsyncData do not complete the page transition until the data fetching is finished, while useLazyFetch and useLazyAsyncData complete the page transition regardless of the data retrieval status.

useFetch is a wrapper function for useAsyncData specifically tailored for using $fetch.

In other words, the following two lines execute equivalent processes:

const { data } = await useAsyncData('count', () => $fetch('/api/users'))
const { data } = await useFetch('/api/users'))

The return value data from useFetch is returned as data wrapped in ref(), so it can be used as a reactive value.
By using useFetch, you can easily describe the data fetching process as follows. It fetches the user list created in API Routes.

<script setup lang="ts">
const { data: users } = await useFetch("/api/users/1");
</script>

<template>
  <h1>User List</h1>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.username }}
    </li>
  </ul>
</template>

By the way, when fetching data from an API created in server/api, the return value of useFetch is automatically typed. This is amazing.

Screenshot 2021-12-12 18.11.30

Pick

useFetch and useAsyncData can do even more amazing things by specifying options.
First is the pick option. Only if the response is an object, you can narrow down the data to be fetched like GraphQL by specifying property names in an array. (I would have been happy if this could also be specified when fetching data as an array...)

<script setup lang="ts">
const { data: user } = await useFetch("/api/users", {
  pick: ["id", "name", "email", "phone"],
});
</script>

<template>
  <h1>{{ user.name }}</h1>
  <ul>
    <li>Email: {{ user.email }}</li>
    <li>Phone: {{ user.phone }}</li>
  </ul>
</template>

transform

This receives a conversion function for the fetched data. In the following example, a process is executed to convert the username of all users to uppercase.

<script setup lang="ts">
const { data: users } = await useFetch("/api/users", {
  transform: (res) => {
    return res.map((user) => ({
      ...user,
      username: user.username.toUpperCase(),
    }));
  },
});
</script>

<template>
  <h1>User List</h1>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.username }}
    </li>
  </ul>
</template>

useState

The function name sounds familiar, but useState performs state management suitable for SSR.

A feature of the Composition API in Vue 3 was the ability to manage state by defining reactive data outside of components. This allowed for writing simple state management without relying on state management libraries like vuex.

In the following example, the state of count can be shared between components by calling useCounter.

  • composables/useCounter.ts
const count = ref(0)

const useCounter = () => {
  const increment = () => count.value++

  const decrement = () => count.value--

  return {
    count: readonly(count),
    increment,
    decrement
  }
}

export default useCounter

This is used within a component by calling it as follows:

<script setup lang="ts">
import useCounter from "~~/composables/useCounter";

const { count, increment, decrement } = useCounter();
</script>

<template>
  <h1>{{ count }}</h1>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</template>

This code works fine when used in a normal Vue 3 application, but it becomes a problem when used in a framework that provides SSR, such as Nuxt.

If reactive data is defined with ref() outside of a component, that state will be shared among all users visiting the application, which could lead to memory leaks.

In Nuxt, you must not use ref() outside the setup function.

As mentioned above, since state management using ref() cannot be done this way in Nuxt, useState is used instead.

useState receives a unique key and a function that returns the initial value as arguments. The value returned by useState is wrapped in a Ref, so it can be used in the same way as when defined with ref().

const useCounter = () => {
  const count = useState('count', () => 0)
  
  const increment = () => count.value++

  const decrement = () => count.value--

  return {
    count: readonly(count),
    increment,
    decrement
  }
}

export default useCounter

When using ref(), it was defined outside the useCounter function to share the state, but with useState, the state can be shared even if it is defined inside the useCounter function.

Composables directory

In Vue 3 Composition API, custom hooks are conventionally placed in the composables directory, but in Nuxt 3, the composables directory has a special meaning.

Functions exported as named exports or default exports within the composables directory can be used within components without an explicit import.

  • composables/useFoo.ts
export const useHoge = () => {
  return 'hogehoge'
}

export default () => {
  return 'foobar'
}
  • pages/index.vue
<script setup lang="ts">
const hoge = useHoge();
const foo = useFoo();
</script>

<template>
  <h1>{{ hoge }}{{ foo }}</h1>
</template>

Plugins directory

Files placed in the plugins directory can now be used without the need to register them in nuxt.config.ts.
Also, adding .client to the filename ensures it runs only on the browser, while adding .server ensures it runs only on the server.

  • plugins/hello.server.ts
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin(nuxtApp => {
  console.log('hello')
})
  • plugins/world.client.ts
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin(nuxtApp => {
  console.log('world')
})

The Plugins directory is executed before all requests, so it can be used as a replacement for Nuxt 2's Middleware (Middleware has been deprecated).

Additionally, nuxtApp.hook() allows you to execute functions passed to the callback at the following specified Nuxt lifecycle stages:

  • app:beforeMount
  • app:created
  • app:mounted
  • app:rendered
  • meta:register
  • page:start
  • page:finish
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.hook('app:mounted', () => console.log('App mounted!'))
})

provide

Inside a plugin, you can provide helpers to the entire Nuxt application by returning an object with a provide property.

  • plugins/http.ts
import { defineNuxtPlugin } from '#app'
import axios from 'axios'

const instance = axios.create({
  baseURL: 'https://api.github.com',
  headers: {
    'Content-Type': 'application/json'
  },
  timeout: 5000
})


export default defineNuxtPlugin(nuxtApp => {
  return {
    provide: {
      http: instance
    }
  }
})

The provided helper can be used as a return value of useNuxtApp.

<script setup lang="ts">
const { $http } = useNuxtApp()

const { data } = await $http.get('/users')
</script>

Even more impressively, the provided helper is automatically typed.

Screenshot 2021-12-12 20.25.01

Final Thoughts

An application I created with Nuxt a year ago has already become an ancient relic...
You don't need complex configurations to include TypeScript, and the fact that features like useFetch are automatically typed is quite impressive; the idea that Nuxt and TypeScript were a bad match is now a thing of the past.

It feels like various features that were in Next.js are being integrated.

One thing I thought was that Nuxt core features, such as auto-imports, have many functions that can be used without an explicit import. I'm not a big fan of such implicit behavior, but I suppose everyone prefers being able to work more easily.

GitHubで編集を提案

Discussion