💻

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

9 min read

ユーザー待望の NuxtVue.js 3.x 対応版となる Nuxt 3 がベータ公開されました。
Nuxt 3 にはほかにも(TypeScript のネイティブサポートをはじめとした)開発体験の向上だったりいくつもの改善があるなかで、もっとも注目したいのは Nitro エンジンによるデータ取得やレンダリング周りの機能向上があります。

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

defer オプションが lazy に変更になったため修正 (2021年11月22日)
default オプションを追加 (2021年11月22日)
useLazyFetch()useLazyAsyncData() を追加 (2021年11月22日)
ClientOnly コンポーネントの実装に伴い、記述を修正 (2021年11月22日)

基本的な使いどころ

Nuxt 2 では Options API の asyncData() や Composition API の useAsync(), useFetch(), useStatic() などを使用し、おもに 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() は次のように利用します。

useAsyncData(key: string, fn: () => Object, options?: { lazy: boolean, server: boolean })
useFetch(url: string, options?)

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

export interface _AsyncData<DataT> {
  data: Ref<DataT>
  pending: Ref<boolean>
  refresh: (force?: boolean) => Promise<void>
  error?: any
}

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

const { data, pending, refresh, 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>

オプションの指定

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

lazy: 非同期関数のプロミスが解決するまでルーティングを待機しない場合は `true` を指定(初期値は `false` )
default: 主に `lazy: true` の際に使用する初期値を返す関数を指定(サーバー側で使用されます)
server: サーバーサイドでは非同期関数を実行しない場合は `false` を指定(初期値は `true` )
transform: 非同期関数の戻り値に対しデータの加工を行う関数を指定
pick: 戻り値がオブジェクトの場合に、指定したキーの値だけに絞り込みを行う場合はキーを配列で指定

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

また、上記は useAsyncData() のオプションで useFetch() ではそれに加え $fetch() のオプション指定が可能です。
なお $fetch() は Nuxt 3 内でグローバルに利用可能な ohmyfetch (様々な環境で利用できる Fetch API )で、そのオプションも指定可能です。

method: Request method を指定(デフォルトは `get` 、 `post` を指定する場合などに使用)
params: 追加のクエリーを指定
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 () => {
  counter++
  return {
    counter,
    renderedOn,
  }
}
components/ServerCount.vue
<script setup lang="ts">
const { data } = await useFetch('/api/count')
</script>

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

const data: Ref<Pick<{
  counter: number;
  renderedOn: number;
}, "counter" | "renderedOn">>

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

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

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

ただし ベータリリース時点では、まだ <ClientOnly> Component は実装されていない ようですのでお気をつけください。(実装されました)

また、現在 Foo.client.vueFoo.server.vue のように Component のファイル名を記述することでクライアント側もしくはサーバー側のみで利用される仕様の検討が行われているようです。

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>

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 により ISG を使用したサイトの開発が可能に

Nitro は Nuxt 3 および Nuxt Bridge を使用した Nuxt 2 のサイトで利用できます。

これまで SSR か SSG か頭を悩ますことがあったと思いますが、これからはとくに理由がなければ設定不要で ISG を利用するということになりそうです。

また、今後 ISR (Incremental Static Regeneration) のようなキャッシュコントロールが可能になる機能が実装されていくと予想しています。
それまでに Serverless functions を活用した開発に慣れておきたいと思います。

(追記) ISR の実装例

未検証のまま追記します。
Nuxt 3 では /server/middleware 内ですべてのリクエストで実行される Server Middleware を作成できます。
次のようにヘッダーを付加することで、自前で Stale-While-Revalidate を設定することが可能です。

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

export default async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
  res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
  next()
}

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

僕の理解力不足により、このページ内の記述において、認識相違や間違いがあるかもしれません。
またベータ版ということで今後に向けて仕様変更が入る可能性は充分にあります。
より正確な情報にアップデートしたいと思いますので、気がついたことがある方はぜひフィードバックをお願いいたします。

Nuxt 3 と Nuxt Bridge のお試しをするサイトを作っています

快速 Nuxt 3
快速 Nuxt Bridge

こちらもよろしくおねがいします。

参考

Discussion

ログインするとコメントできます