【Nuxt.js】useFetch・useAsyncDataでデータを取得する(違いも解説)

JavaScript

こんにちは、かつコーチです。

Nuxt.jsでAPIからデータを取得しようとすると、必ず出てくるのが useFetchuseAsyncData という2つのComposable(Composable:Vueの合成関数。状態やロジックをまとめて再利用できる仕組みのことです)です。

「どっちを使えばいいの?」「名前が似ていて紛らわしい」と感じた人も多いのではないでしょうか。

この記事では、useFetchuseAsyncData の違いを比較表で整理しながら、それぞれの基本の書き方と、SSRでハマりやすいポイントを解説します。

useFetch・useAsyncDataとは?

2つとも「データ取得を安全にするComposable」

useFetchuseAsyncData は、どちらもNuxtが用意しているデータフェッチ用Composableです。

データフェッチとは、APIサーバーなどの外部からデータを取得する処理のことです。

普通の非同期処理(fetchaxios をそのまま使うなど)と違い、この2つはNuxtのSSRSSR:Server Side Renderingの略。サーバー側でHTMLを組み立ててからブラウザに返す仕組み)と連携しています。

サーバーで取得したデータをそのままブラウザ側に引き継いでくれるので、同じAPIリクエストが二重に発生するのを防いでくれます。

なぜ2種類も用意されているのか

useFetch は「URLを渡すだけで手軽に使える」ことを目指したComposableです。

一方の useAsyncData は、その内部で実際に使われている土台のComposableで、より柔軟な非同期処理に対応できます。

つまり useFetchuseAsyncData を使いやすくラップしたものだと考えると理解しやすくなります。

基本の書き方

手順1:useFetchでシンプルに取得する

まずは useFetch の基本形です。

<!-- pages/posts/index.vue -->
<script setup lang="ts">
interface Post {
  id: number
  title: string
  body: string
}

const { data: posts, pending, error } = await useFetch<Post[]>("/api/posts")
</script>

<template>
  <div v-if="pending">読み込み中です...</div>
  <div v-else-if="error">エラーが発生しました:{{ error.message }}</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">
      {{ post.title }}
    </li>
  </ul>
</template>

URLの文字列を渡すだけで、data(取得結果)・pending(読み込み中フラグ)・error(エラー情報)が手に入ります。

裏側では内部的に $fetch が呼ばれており、URLと引数の内容から自動的にキャッシュ用のキーが作られます。

手順2:useAsyncDataで柔軟に取得する

次に useAsyncData です。

<!-- pages/posts/index.vue -->
<script setup lang="ts">
interface Post {
  id: number
  title: string
  body: string
}

const { data: posts, pending, error } = await useAsyncData<Post[]>(
  "posts-list",
  () => $fetch("/api/posts")
)
</script>

第1引数にキー(今回は "posts-list")、第2引数に実際の処理内容を関数として渡します。

この形にすることで、$fetch だけでなく、複数のAPIを組み合わせたり、外部SDKの呼び出しを混ぜたりする処理も1つの結果としてまとめられます。

<script setup lang="ts">
interface DashboardData {
  posts: { id: number; title: string }[]
  userCount: number
}

const { data } = await useAsyncData<DashboardData>("dashboard", async () => {
  const [posts, userCount] = await Promise.all([
    $fetch<{ id: number; title: string }[]>("/api/posts"),
    $fetch<number>("/api/users/count"),
  ])
  return { posts, userCount }
})
</script>

このように複数のリクエストをまとめて1つのデータとして扱いたい場合は、useFetch ではなく useAsyncData を選びます。

比較表で見る違い

項目useFetchuseAsyncData
主な用途単一のAPIをURL指定で取得複数の処理を組み合わせた柔軟な取得
キーの指定URL等から自動生成(省略可)第1引数で必須(自分で命名)
内部の実体useAsyncData + $fetchのラッパーNuxtのデータ取得の土台となるComposable
複数リクエストの合成不向き(1リクエスト向け)得意(Promise.allなどを内部で自由に書ける)
手軽さ高い(URLだけで完結)やや低い(handlerを自分で書く)

迷ったときの判断軸はシンプルです。

「1本のAPIをそのまま取得するだけ」なら useFetch、「複数の処理を組み合わせて1つの結果にしたい」なら useAsyncData を選ぶと、遠回りせずに済みます。

SSRでのデータ取得の仕組み

Nuxtアプリを開くと、まずサーバー側でページが描画され、そのときに useFetchuseAsyncData のリクエストも一緒に実行されます。

サーバーで取得したデータは、ペイロードペイロード:サーバーからブラウザに渡される初期データのこと)としてHTMLに埋め込まれた状態でブラウザに送られます。

ブラウザ側でNuxtアプリが起動する(ハイドレーション:静的なHTMLにVueの機能を後から結びつける処理)ときは、このペイロードを再利用するため、同じAPIリクエストをもう一度ブラウザから送る必要がありません。

ここがキーの役割で、サーバー側とブラウザ側で同じキーが使われて初めて「同じデータ」だとNuxtが認識し、二重リクエストを防いでくれます。

よくあるつまずきポイント・エラー対処

Before/After:条件分岐の中で呼び出して二重実行になった話

私が実際にハマったのは、ログイン状態によって表示を切り替えるページで useFetch を使ったときでした。

❌ Before:if文の中でuseFetchを呼び出す

<script setup lang="ts">
const { loggedIn } = useAuthState()

// これはやってはいけない書き方
if (loggedIn.value) {
  const { data } = await useFetch("/api/dashboard")
}
</script>

このコードを動かすと、ブラウザのコンソールに次のような警告が出ました。

[nuxt] `useFetch` should not be called more than once per component with the same key. Please provide a unique key.

原因は、useFetchuseAsyncData がVueの refcomputed などと同じComposition APIのルールに従っており、コンポーネントの直下(トップレベル)で呼び出す必要があるためです。

条件分岐や関数の中に隠してしまうと、サーバー側とブラウザ側で呼び出されるタイミングがずれてしまい、キーの整合性が崩れてしまいます。

✅ After:トップレベルで呼び出し、条件はimmediate/watchで制御する

<script setup lang="ts">
const { loggedIn } = useAuthState()

const { data, execute } = await useFetch("/api/dashboard", {
  immediate: false,
})

watch(
  loggedIn,
  (value) => {
    if (value) execute()
  },
  { immediate: true }
)
</script>

useFetch 自体は必ずトップレベルで呼び出しておき、実行するかどうかを immediate: falseexecute() で制御する形に変えました。

こうすることで、サーバー・ブラウザどちらでも同じ場所で同じキーの useFetch が呼ばれるようになり、警告が出なくなりました。

応用・一歩先の使い方

$fetchとuseFetchの使い分け

もう1つ整理しておきたいのが $fetch との使い分けです。

$fetch はNuxtに組み込まれた素のHTTPクライアントで、SSRとの連携機能を持ちません。

useFetchuseAsyncData は「ページを開いたときに表示するための初期データ取得」に向いており、SSRとの連携やキャッシュの恩恵を受けられます。

一方の $fetch は「ボタンを押したときにPOSTを送る」「フォーム送信後にAPIを呼ぶ」といった、ユーザー操作をきっかけに発生する単発の通信に向いています。

<script setup lang="ts">
interface CreatePostInput {
  title: string
  body: string
}

async function handleSubmit(input: CreatePostInput): Promise<void> {
  await $fetch("/api/posts", {
    method: "POST",
    body: input,
  })
}
</script>

このように、ページ表示に使うデータ取得は useFetch / useAsyncData、ユーザー操作をきっかけとする通信は $fetch と使い分けると、SSRの恩恵を無駄にせずに済みます。

キー(key)の重要性を理解する

useFetch はキーを省略できますが、実務ではURL・メソッド・パラメータをもとに自動生成されたキーに頼りきりにしないほうが安全です。

動的なURLを扱うページ(例:/posts/[id])では、明示的にキーを指定しておくと、意図しないキャッシュの使い回しを防げます。

<script setup lang="ts">
const route = useRoute()
const postId = computed(() => route.params.id as string)

const { data: post } = await useFetch(`/api/posts/${postId.value}`, {
  key: `post-${postId.value}`,
})
</script>

キーを明示しておくことで、後からコードを読む人にも「このデータ取得が何のためのものか」が伝わりやすくなるメリットもあります。

まとめ

この記事のポイント

  • useFetch はURL指定で手軽に使える、useAsyncData を内部で使ったラッパーComposable
  • useAsyncData は複数の処理を組み合わせられる、より柔軟なComposable
  • どちらもSSRとブラウザ間でデータを共有し、二重リクエストを防いでくれる
  • useFetch / useAsyncData はコンポーネントのトップレベルで呼び出す必要があり、条件分岐で囲むと警告や不整合の原因になる
  • ページ初期表示のデータ取得には useFetch / useAsyncData、ユーザー操作起点の通信には $fetch を使い分ける

次に読むべき記事

データの取得方法が分かったら、次は取得先となるAPI自体をNuxtアプリの中に作る方法を知っておくと便利です。

→ 次の記事:server/apiディレクトリでサーバーAPIを作る

タイトルとURLをコピーしました