🍩

Nuxt3のuseFetchとawait

2024/01/26に公開

Nuxt3の useFetch は 以下のようなPromiseオブジェクトを返します。

Promise<AsyncData<DataT, ErrorT>>

Promiseオブジェクトであるため async / await が利用可能です。

Nuxt.jsはTop-Level Awaitに対応していますので、<script setup>のルートではasync宣言をしなくてもawaitが利用可能です。

<template>
  <pre>{{ data.time }}</pre>
</template>

<script setup>
const { data } = await useFetch('/api/lazy');
</script>

awaitは処理をブロックする

JavaScriptの基本的なおさらいから入りますがawaitは非同期処理が解決されるまでJavaScriptの処理をストップします。

実際の挙動を見ていきましょう。

まずは、server/api/lazy.tsとして以下のレスポンスに2秒ぐらいかかかるAPIを用意します。

server/api/lazy.ts
export default defineEventHandler(async () => {
  const time = new Date()
  await new Promise(resolve => setTimeout(resolve, 2000));
  return {
    time
  }
})

次に、pages/lazy.vueとして複数回APIにアクセスしているページを用意します。
pages/lazy.vueではAPI通信ごとに何秒かかったかをconsole.timeLog()で出力しています。

pages/lazy.vue
<template>
  <pre>{{ data1.time }}</pre>
  <pre>{{ data2.time }}</pre>
  <pre>{{ data3.time }}</pre>
  <pre>{{ data4.time }}</pre>
</template>

<script setup>

console.time()

const { data: data1 } = await useFetch('/api/lazy')
console.timeLog()

const { data: data2 } = await useFetch('/api/lazy')
console.timeLog()

const { data: data3 } = await useFetch('/api/lazy')
console.timeLog()

const { data: data4 } = await useFetch('/api/lazy')
console.timeEnd()

</script>

SSRのログを確認すると以下のようになっています。

default: 0.004ms
default: 2.003s
default: 4.006s
default: 6.010s
default: 8.015s

ページ表示の処理が終わるまで8秒ほどの時間がかかったのがわかります。

すべての通信を同期通知に変換してしまうとページ表示まで時間がかるため、通信は非同期処理として並列にAPIアクセスするようにしておきたいです。

Promise.allで解決する

一般的なJavaScriptアプリケーションではこの問題を解決するためにPromise.allを活用します。
Nuxt.jsではもう少しスマートな解決方法が用意されていますのでその方法は後ほど解説します。

以下のようにPromise.allの引数にPromiseオブジェクトを配列で指定することで、すべての非同期処理として並列におこないすべての非同期処理がおわるまでまって処理を再開します。

pages/lazy.vue
<template>
  <pre>{{ data1.time }}</pre>
  <pre>{{ data2.time }}</pre>
  <pre>{{ data3.time }}</pre>
  <pre>{{ data4.time }}</pre>
</template>

<script setup>

console.time()
console.timeLog()

const response = await Promise.all([
  useFetch('/api/lazy'),
  useFetch('/api/lazy'),
  useFetch('/api/lazy'),
  useFetch('/api/lazy')
])

const [
  { data: data1 } ,
  { data: data2 } ,
  { data: data3 } ,
  { data: data4 }
] = response

console.timeEnd()

</script>

SSRのログを確認すると以下のようになっています。

default: 0.004ms
default: 2.007s

ページ表示の処理が終わるまで2秒で済んだのがわかります。

Promise.allはパフォーマンスの問題を解決しますがコードが複雑になってしまうのが難点です。

この問題はNuxt.jsではもうすこしスマートに解決することができます。

awaitを取る

Nuxt.jsでは当初のコードからawaitを取るだけでPromise.allと同様の挙動にすることができます。

pages/lazy.vue
<template>
  <pre>{{ data1.time }}</pre>
  <pre>{{ data2.time }}</pre>
  <pre>{{ data3.time }}</pre>
  <pre>{{ data4.time }}</pre>
</template>

<script setup>

console.time()

const { data: data1 } = useFetch('/api/lazy')
console.timeLog()

const { data: data2 } = useFetch('/api/lazy')
console.timeLog()

const { data: data3 } = useFetch('/api/lazy')
console.timeLog()

const { data: data4 } = useFetch('/api/lazy')
console.timeEnd()

</script>

このスクリプトのSSRのログを確認すると以下のようになっています。

default: 0.005ms
default: 0.432ms
default: 1.012ms
default: 1.457ms
default: 1.99ms

<script>の処理時間はほとんど発生していないことが確認できます。

では、即座にページが表示されるかというとそうではなくて、Nuxt.jsではuseFetchで発生した非同期通信が解決されるまでページの描画は行いません。

そのためページの描画はPromise.allは利用した場合と同じぐらいの2秒ほどになります。

Promise.allと比較すると直感的なコードのまま非同期通信を利用できるようになったと感じるでしょう。

useFetchのawait lessの注意点

awaitを使わずにuseFetchを利用した場合はlazyオプションを利用した場合とほぼ同じ挙動になのでその点は注意が必要です。

lazyオプションをつけたときと同様にSSR時は非同期通信が解決されるまでページの描画は行いませんがページ遷移などのCSR時は非同期通信が解決される前にページの描画を行ってしまいます。

以下のような遷移元のページを用意して

pages/index.vue
<template>
  <NuxtLink to="/lazy">表示の遅いページに遷移</NuxtLink>
</template>

pages/lazy.vueにアクセスすると以下のようなエラーが表示されます。

これはAPIのデータの取得が完了していないタイミングでページを表示したためdata1.timeなんてないよと怒られてしまっています。

これにはいくつかの解決方法があります

返却値ではオプショナルチェーンを利用する

非同期通信が解決されるまで返却値はnullであるためdata1?という具合に表示時にオプショナルチェーンを利用すると解決が可能です。

pages/lazy.vue
<template>
  <pre>{{ data1?.time }}</pre>
  <pre>{{ data2?.time }}</pre>
  <pre>{{ data3?.time }}</pre>
  <pre>{{ data4?.time }}</pre>
</template>

条件分岐を行う

数が多い場合はオプショナルチェーンを利用するのは手間でしょう。
その場合は直前にv-ifで存在の確認をするのがよいでしょう。

pages/lazy.vue
<template>
  <pre v-if="data1">{{ data1.time }}</pre>
  <pre v-if="data2">{{ data2.time }}</pre>
  <pre v-if="data3">{{ data3.time }}</pre>
  <pre v-if="data4">{{ data4.time }}</pre>
</template>

このコードの例がAPIのレスポンスが1データしかないので微妙ですが以下のように複数の値を参照しているときに有効です。

<template>
  <div v-if="data">
    <h1>{{ data.title }}</h1>
    <p>{{ data.description }}</p>
    <p>{{ data.time }}</p>
    ...
    ...
    <p>{{ data.author }}</p>
  </div>
</template>

結論

Nuxt.jsでページを作成する場合は必要がなければawaitはなしでuseFetchを利用するのがよいでしょう。

ただし、レスポンスがリアクティブなデータで値が入る前まではnullであることは注意が必要です。

株式会社トゥーアール

Discussion