💻

Nuxt 3 の useFetch() と useAsyncData() の使い方

2021/10/24に公開

ユーザー待望の Nuxt 3 が公開されました。
(Vue 2 対応だった Nuxt 2Vue.js 3 対応版です)

Nuxt 3 は、TypeScript のネイティブサポートをはじめとした開発体験の向上など、多数の改善がなされています。
もっとも注目したいのは Nitro によるデータ取得やレンダリング周りの機能向上があります。

Nitro により、いわゆる ISG (Incremental Static Generation インクリメンタル静的ページ生成)も可能です。Nuxt 3 は、とてもシンプルに(意識することなく)活用することができるようになっています。

基本的な使いどころ

Nuxt 2 では Options API の asyncData() や Composition API の useAsync(), useFetch(), useStatic() などを使用し、(おもに SSR, SSG において)画面描画に必要なデータを非同期に取得する使い方をしてきました。
Nuxt 3 ではそれらが新たに useFetch()useAsyncData() のふたつにアップデートされています。
(データ取得時に画面描画をブロックしない useLazyFetch()useLazyAsyncData() もあります)

同じようにみえるのですが Nuxt 3 の useFetch() (以下断りのない場合はすべて Nuxt 3 のもの) と useAsyncData() は、その使いどころを含めて改善されています。

従来のように Page Component で取得したデータを props を使って下位の Component に引き渡す必要はなくなりました。

/pages/posts.vue
<script setup lang="ts">
const { data: posts } = await useFetch('/api/posts')
</script>

<template>
  <div>
    <h1>投稿一覧</h1>
    <PostList :posts="posts" />
  </div>
</template>

Vue.js 3.2 で導入された <script setup> のシンプルな書き味に驚くのですが、それ以上に useFetch() がたったこれだけで(オブジェクトの配列を /server/api/posts.ts で返す場合)PostList.vue に引き渡すことができます。

が、これは今後の標準的な書き方ではなくなっていくでしょう。
代わりに(Page Component ではなく)通常の Component で useFetch() を使いましょう。

components/PostList.vue
<script setup lang="ts">
const { data: posts } = await useFetch('/api/posts')
</script>

<template>
  <div>
    <h1>投稿一覧</h1>
    <PostListItem
      v-for="post in posts"
      :key="post.id"
      :post="post"
    />
  </div>
</template>

Page Component は次のようにシンプルになります。

/pages/posts.vue
<template>
  <div>
    <PostList/>
  </div>
</template>

これだけで PostList Component の await によるプロミスが解決されるまで Page Component は適切に描画を停止します。
もちろん複数の子 Component を使っていて、それぞれで await している場合も同様です。
Nitro が fetch するデータを適切にキャッシュしてくれますので、これまでのように useStatic() を使うのか asyncData() でやるのかなど考える必要はなくなりました。

Page Component には複雑なロジックを書かず、ページレイアウトを記述するだけのラッパー的な存在になっていくかもしれません。

useFetch() と useAsyncData()

useFetch()useAsyncData() のふたつの違いが気になりますが useFetch() はデータ取得に特化した useAsyncData() のラッパーでしかありません。
両者に本質的な違いはなく useAsyncData(key, () => $fetch(path), options) のように useAsyncData() の第2引数が () => $fetch() になる場合はすべて useFetch() で構いません。
データ取得以外のケースで非同期に処理をするケースというのは限定されることから useFetch() を使用するということで足りることが多いでしょう。

というわけで、以下 useFetch() の利用例で見ていきます。
useAsyncData() が必要な場合は、第2引数にその処理を実行する関数を渡してください。

基本的な使い方

useAsyncData()useFetch() は次のような Composable Function です。

function useAsyncData(
  key?: string,
  handler: (ctx?: NuxtApp) => Promise<DataT>,
  options?: AsyncDataOptions<DataT, Transform, PickKeys>
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null>

function useFetch(
  request: Ref<ReqT> | ReqT | (() => ReqT),
  opts?: UseFetchOptions<_ResT, Transform, PickKeys>
)

戻り値の型定義は次のようになっています(両者共通です)

export interface _AsyncData<DataT, ErrorT> {
  data: Ref<DataT | null>
  pending: Ref<boolean>
  refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
  execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
  error: Ref<ErrorT | null>
}

export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>

何もオプションを使わない場合の実装例は次のようになるでしょう。

const { data, pending, refresh, execute, error } = await useFetch('/api/post/123')

基本的には { data: post } のように記述し post にデータがリアクティブな値として入ることを期待しておけばOKです。
(念の為 JavaScript に慣れてない方のために補足すると data の代わりに post という変数名で受け取る記述です)

components/PostItem.vue
<script setup lang="ts">
const { data: post } = await useFetch('/api/post/123')
console.log(post.value.id) // script 内では .value をつける
</script>

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.body }}</p>
  </div>
</template>

オプションの指定

公式ドキュメントによると、オプションには次のような指定が可能です。

  • key:
    Nuxt が fetch したデータをキャッシュする際に使用するキー(useFetch() のデフォルトはリクエストのパスを元にして自動生成。useAsyncData() のデフォルトは実行されたコードのファイル名や行番号から自動生成)
  • server:
    サーバー側では非同期関数を実行しない場合は false を指定(初期値は true
  • lazy:
    非同期関数のプロミスが解決するまでルーティングを待機しない場合は true を指定(初期値は false
  • default:
    主に lazy: true の際に使用する初期値を返す関数を指定(サーバー側で使用されます)
  • pick:
    戻り値がオブジェクトの場合に、指定したキーの値だけに絞り込みを行う場合はキーを配列で指定
  • watch:
    自動的に refresh() を行うために watch するリアクティブな値
  • transform:
    非同期関数の戻り値に対しデータの加工を行う関数を指定
  • immediate:
    即時に実行しない場合は false を指定する(execute() を使い任意のタイミングで実行したい場合)

なお pick は transform の後に行われます。
また Stable リリース時点では、戻り値がオブジェクトの配列で、各要素のオブジェクトのキーを pick で指定するような仕様にはなっていません。
その場合は transform ですべての処理を行う必要があります。

また、上記は useAsyncData() のオプションで useFetch() ではそれに加え $fetch() のオプション指定が可能です。
なお $fetch() は Nuxt 3 内でグローバルに利用可能な unjs/ofetch (node.js等の様々な環境で利用でき、かつJSON等をパースした戻り値を得られる Fetch API)で、そのオプションも指定可能です。

  • method: 'POST' 'PUT' などの Request method を指定(初期値は 'GET')
  • query: クエリー { id: 123 } のように指定可能
  • params: query のエイリアス
  • body: リクエストボディ { some: 'json' } のように指定可能
  • headers: リクエストヘッダー { 'Cache-Control': 'no-cache' } のように指定可能
  • baseURL: Base URL を指定

第1引数の url (key) は、キャッシュ時のキーとしても使われます。
そのためキャッシュに含めたい場合は /api/posts?lang=ja のように指定しておき params には別途必要なものを { order: desc } のように加えるために使用することになるでしょう。

/server/api で返した値は useFetch() で自動的に型がつく

/server/api 以下のディレクトリにデータ取得用の API を作成した場合、そこで返却されるデータについては useFetch() を記述する際に型を確認することができます。

server/api/count.ts
let counter = 0
const renderedOn = (new Date()).getTime()

export default defineEventHandler((event) => {
  counter++
  return {
    counter,
    renderedOn,
  }
})
components/ServerCount.vue
<script setup lang="ts">
const { data } = await useFetch('/api/count')
</script>

この data について次のような型を確認することができます。

const data: Ref<{
  counter: number;
  renderedOn: number;
} | null>

この型付けがどのように行われているかについては、つぎの記事が参考になります。

<ClientOnly> Component と組み合わせ private なデータを取得する

Universal Rendering (SSR) 時にサーバー側にてデータを取得する場合、原則的にユーザー固有の情報を含めないように注意する必要があります。
Nuxt では組み込みの <ClientOnly> Component によってそれを簡単に実現できます。

Nuxt 3 では Foo.client.vue とファイル名の拡張子を .client.vue にすることで <ClientOnly> を使わずにサーバー側では実行されないコンポーネントを作成可能です。

components/MyUserInfo.vue
<script setup lang="ts">
const { userId } = useRouter().params
const { data: user } = await useFetch(`/api/user/${userId}`)
</script>

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

これを上位の Component で表示します。

components/MyUser.vue
<template>
  <div>
    <h1>マイページ</h1>
    <ClientOnly>
      <MyPage/>
      <template #fallback>
        <!-- Server 側でレンダリングされ Client 側で差し替えられます -->
        <p>読込中…</p>
      </template>
    </ClientOnly>
  </div>
</template>

useLazyFetch()useLazyAsyncData() による記述

lazy オプションを true にセットするか、もしくは useLazyFetch()useLazyAsyncData() を使用することで同様の記述が可能です。
default オプションに設定する初期値が Server 側で描画されます。

components/MyUserInfo.vue
<script setup lang="ts">
const { userId } = useRouter().params
const defaultUser = () => ({ name: 'no name', email: 'info@example.com' })
const { data: user } = useLazyFetch(`/api/user/${userId}`, { default: defaultUser })
</script>

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

pending を使って表示を制御する

lazy オプション使用時(もしくは useLazyFetch(), useLazyAsyncData() 使用時)に pending の値を使用しシンプルにロード中の画面を出すことができます。

components/PostList.vue
<script setup lang="ts">
const { data: posts, pending } = useFetch('/api/posts', { lazy: true })
</script>

<template>
  <div v-if="pending">
    Loading ...
  </div>
  <div v-else>
    <PostListItem v-for="post in posts" :key="post.id" />
  </div>
</template>

error を使ってエラーページを表示する

createError() を使用し 404500 等のエラーページを出すことができます。

components/PostList.vue
<script setup lang="ts">
const { data: posts, error } = await useFetch('/api/posts')

if (!posts.value || error.value) {
  throw createError({
    statusCode: 404,
    message: 'PostList not found',
  })
}
</script>

<template>
  <div>
    <PostListItem v-for="post in posts" :key="post.id" />
  </div>
</template>

refresh() データの再取得

refresh を使用し、任意のタイミングでデータ再取得が可能です。

components/PostList.vue
<script setup lang="ts">
const { data: posts, refresh } = await useFetch('/api/posts')
</script>

<template>
  <div>
    <PostListItem v-for="post in posts" :key="post.id" />
    <button @click="refresh">リスト更新</button>
  </div>
</template>

データ取得中(pending)のタイミングで refresh() すると、先に実行された Promise が解決しても(すべての Promise が解決されるまで) data は更新されません。
段階的に更新結果を表示する場合などは refresh({ dedupe: true }) と呼び出す必要があります。

watch オプション

更新のきっかけがリアクティブな値に設定できる場合は watch オプションが便利です。
たとえばURLのクエリーが更新された場合などに利用可能でしょう。

components/PostList.vue
<script setup lang="ts">
const q = ref(useRoute().query.q || '')
const { data: posts } = await useFetch('/api/posts', {
  query: { q: q.value },
  watch: q, // 内部で watch() の第1引数として使用され、変更時に refresh
})
</script>

<template>
  <div>
    <input v-model="q" />
    <h1>投稿一覧</h1>
    <PostListItem
      v-for="post in posts"
      :key="post.id"
      :post="post"
    />
  </div>
</template>

refreshNuxtData(), clearNuxtData() でページ内のデータ再取得・キャッシュクリア

refreshNuxtData() はすべてのデータを再取得し、ページを更新するとともに、useAsyncData、useLazyAsyncData、useFetch、useLazyFetchのキャッシュをクリアします。
Nuxt 3 の Composable Function で、ページ内で使用している useFetch() useAsyncData() に対して有効です。

clearNuxtData() は、キャッシュ と Error ステータスおよび useAsyncData()useFetch() の保留中のプロミスを削除します。
別のページで取得したデータのキャッシュを無効化したい場合に便利です。

refreshNuxtData(keys?: string | string[])
clearNuxtData (keys?: string | string[] | ((key: string) => boolean)): void

引数無しですべてに作用します。

Nuxt Bridge での利用

Nuxt Bridge では Nuxt 3 の useFetch()useAsyncData() を使用することはできませんが useLazyFetch()useLazyAsyncData() については下記のように書き換えが可能です。

components/SomethingNuxtTwo.vue
<script setup lang="ts">
import { useFetch } from '@nuxtjs/composition-api'
const posts = ref([])
const { fetch } = useFetch(() => { posts.value = await $fetch('/api/posts') })
function updatePosts() {
  return fetch()
}
</script>

上記を、次のように変更できます。

components/SomethingNuxtTwo.vue
<script setup lang="ts">
import { useLazyAsyncData, useLazyFetch } from '#app'
const { data: posts, refresh } = useLazyFetch('/api/posts')
// ↓ も同じ
// const { data: posts, refresh } = useLazyAsyncData('posts', () => $fetch('/api/posts'))
function updatePosts() {
  return refresh()
}
</script>

Nitro により Universal Rendering (SSR, ISG, ISR) を使用したサイトを CDN 環境で公開可能に

Nitro は Nuxt 3 および Nuxt Bridge を使用した Nuxt 2 のサイトで利用できます。
軽量な Web サーバーが含まれてビルドされるので、動作可能な環境が増えました。

これまで Universal Rendering (SSR) か SSG か頭を悩ますことがあったと思います。
CDN を活用したホスティングサービスで Serverless な Function を利用できるようになりました。

routeRules による Hybrid Rendering

Nuxt 3 は、ルーティングごとにレンダリングの方式とキャッシュの方式を選べます(Hybrid Rendering)

サーバー側のキャッシュの設定により ISR (Incremental Static Regeneration) のようなキャッシュコントロールも可能です。

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // サーバー側でレンダリングし、必要に応じてバックグラウンドで更新する設定
    '/blog/**': { swr: true },
    '/article/**': { swr: 600 },
    // SSG サーバー側で(ビルド時に)レンダリングし、以降それを利用する設定
    '/about': { static: true },
    '/news/**': { cache: { /* cache options*/ } },
    // CSR クライアント側でのみ描画する設定
    '/admin/**': { ssr: false },
  },
})

Nuxt 3 はとても柔軟なレンダリングが可能になりました。

フィードバックをぜひください

僕の理解力不足により、このページ内の記述において、認識相違や間違いがあるかもしれません。
より正確な情報にアップデートしたいと思いますので、気がついたことがある方はぜひフィードバックをお願いいたします。

参考

Discussion