意外と知られていない必ず覚えるべきNuxtのuseFetchの使い方

NuxtのuseFetchとは

Nuxtでの開発においてAPIリクエストの要となる「useFetch」。

基本的な使い方をマスターしている人は多いですが、意外と見落とされがちな「知っておかないとハマる仕様」がいくつか存在します。

refをURLに組み込むなら関数(getter)で渡す

useFetchのURLにrefの値を組み込む場合は、テンプレート文字列でそのまま渡すと、refの値が変更されても再取得されません。

// ❌️ 間違い
const productId = ref(1)
const { data } = await useFetch(`https://dummyjson.com/products/${productId.value}`)

この場合は関数(getter形式)にして渡すのが正解です。

// ✅️ 正しい
const productId = ref(1)
const { data } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`
)

試しに以下のようなコードでselectで変更すると、値が切り替わった瞬間に自動で新しいURLへ再度リクエストが送信されるようになります。

app.vue
<script setup lang="ts">
type Product = {
  id: number
  title: string
  thumbnail: string
}

const productId = ref(1)
const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`
)
</script>

<template>
  <h1>商品ページ</h1>
  <h2>productId: {{ productId }}</h2>
  <select v-model="productId">
    <option :value="1">1</option>
    <option :value="2">2</option>
    <option :value="3">3</option>
  </select>
  <p v-if="status === 'pending'">loading...</p>
  <p v-else-if="status === 'error'">error!</p>
  <p v-else-if="status === 'success' && data">
    {{ data.title }}<br>
    <img :src="data.thumbnail" :alt="data.title" width="200" height="200" />
  </p>
</template>

NuxtのuseFetchでAPIリクエストが切り替わるサンプル

pickでレスポンスのデータ容量を削減する

https://dummyjson.com/products/1 のレスポンスには様々なキーと値が含まれています。

この記事のサンプルのようにid, title, thumbnailしか使用しない場合はpickで抽出する内容を指定したほうが、データ容量を削減でき、可読性も高まります。

const productId = ref(1)
const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    pick: ['id', 'title', 'thumbnail'],
  }
)

二重リクエストを防ぐkeyオプションを追加する

useFetchは同じデータを複数のコンポーネントで共有・参照したい場合、同一のkeyを指定しておくことで無駄な再リクエストを回避できます。

const productId = ref(1)
const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    pick: ['id', 'title', 'thumbnail'],
    key: () => `product-${productId.value}`,
  }
)

通信状態はstatusを参照する

従来の pending(boolean)だけでなく、Nuxt 4ではpendingを使用せずstatusで通信状態を参照することが推奨されています。

// ❌️ 間違い
const productId = ref(1)
const { data, pending } = await useFetch(`https://dummyjson.com/products/${productId.value}`)
// ✅️ 正しい
const productId = ref(1)
const { data, status } = await useFetch(`https://dummyjson.com/products/${productId.value}`)

pending, error, successを書くことで、通信状態別の処理がわかりやすくなります。

app.vue
<p v-if="status === 'pending'">loading...</p>
<p v-else-if="status === 'error'">error!</p>
<p v-else-if="status === 'success' && data">
  {{ data.title }}<br />
  <img :src="data.thumbnail" :alt="data.title" width="200" height="200" />
</p>

未選択(null)がある場合はenabledを使用する

前述のサンプルはproductIdに「1」が設定されていますが、selectに未選択の選択肢が存在する場合はproductIdを「null」にします。

しかし、そのままだとnullがproductIdに設定されてリクエストされてしまうため、初期値が未選択(null)の場合はenabledを追加して、nullでなければリクエストするように設定します。

const productId = ref<number | null>(null)
const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    pick: ['id', 'title', 'thumbnail'],
    key: () => `product-${productId.value}`,
    enabled: () => productId.value !== null,
  }
)

NuxtのuseFetchでAPIリクエストが切り替わるサンプル(初期選択なし)

ボタン押下時のAPIリクエストはexecuteを使用

前述のサンプルはselectで選択してAPIリクエストをしていますが、一般的にはボタン押下時にAPIリクエストを発生させたいケースが多いです。

useFetchからexecuteを以下のように受け取って、ボタン押下時にexecute()を実行すればAPIリクエストが可能です。

keyは product-${productId.value} を指定すると、select切り替え時に表示した商品のデータが画面から消えるため、'product' を設定します。

また、「immediate: false」で初期表示での自動リクエストを無効化、「watch: false」でproductIdが変更されたときの自動リクエストを無効化しています。

app.vue
<script setup lang="ts">
type Product = {
  id: number
  title: string
  thumbnail: string
}

const productId = ref<number | null>(null)
const { data, status, execute } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    pick: ['id', 'title', 'thumbnail'],
    key: 'product',
    enabled: () => productId.value !== null,
		immediate: false,
		watch: false,
  }
)
</script>

<template>
  <h1>商品ページ</h1>
  <h2>productId: {{ productId }}</h2>
  <select v-model="productId">
    <option :value="null">--</option>
    <option :value="1">1</option>
    <option :value="2">2</option>
    <option :value="3">3</option>
  </select>
		<button :disabled="status === 'pending'" @click="() => execute()">
		商品データ取得
	</button>
  <p v-if="status === 'pending'">loading...</p>
  <p v-else-if="status === 'error'">error!</p>
  <p v-else-if="status === 'success' && data">
    {{ data.title }}<br />
    <img :src="data.thumbnail" :alt="data.title" width="200" height="200" />
  </p>
</template>

NuxtのuseFetchでボタン押下時にAPIリクエストが発生するサンプル

setup直下以外で呼んではいけない

useFetchはコンポーネントのsetupやプラグイン、ルートミドルウェアといった「Nuxt/Vueのコンテキストが確立している場所」で同期的に呼び出す必要があります。

onMounted内、イベントハンドラ内、if文やforループの中、awaitの後などで呼び出すと、内部でコンテキストが取得できずエラーや警告が出ます。

条件分岐したい場合は呼び出し自体は常に行い、enabledオプションで制御するのが正しい書き方です。

// ❌️ 間違い
onMounted(async () => {
  const { data } = await useFetch('/api/products')
})

// ❌️ 間違い
function fetchProducts() {
  const { data } = useFetch('/api/products')
}

// ❌️ 間違い
if (foo) {
  const { data } = useFetch('/api/products')
}

遅いAPIの場合はlazy: trueを使用する

デフォルト(lazy: false)ではページ遷移がデータ取得完了までブロックされます。

遅いAPIをこのまま使うと画面遷移がもたつくので、lazy: true(またはuseLazyFetch)にしてstatusを見ながら独自にローディング表示する方が体感速度は良くなります。

const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    lazy: true,
  }
)

dataの中身をそのまま書き換えない

useFetchが返すdataはデフォルトでshallowRefです。

パフォーマンスのためですが、ネストしたプロパティを直接書き換えても再描画されないことがあります。

深いリアクティビティが必要なら「deep: true」を指定します。

const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    deep: true,
  }
)

SSRで不要ならserver: falseを設定する

ログイン後にしか使わないデータなど、SSR時に取得する必要がないものまでuseFetchしていると、サーバー側で無駄なリクエストが発生します。

クライアントでのみ取得したい場合は「server: false」を指定します。

const { data, status } = await useFetch<Product>(
  () => `https://dummyjson.com/products/${productId.value}`,
  {
    server: false,
  }
)
カテゴリーNuxt