こんにちは、かつコーチです。
Nuxt.jsでAPIからデータを取得しようとすると、必ず出てくるのが useFetch と useAsyncData という2つのComposable(Composable:Vueの合成関数。状態やロジックをまとめて再利用できる仕組みのことです)です。
「どっちを使えばいいの?」「名前が似ていて紛らわしい」と感じた人も多いのではないでしょうか。
この記事では、useFetch と useAsyncData の違いを比較表で整理しながら、それぞれの基本の書き方と、SSRでハマりやすいポイントを解説します。
useFetch・useAsyncDataとは?
2つとも「データ取得を安全にするComposable」
useFetch と useAsyncData は、どちらもNuxtが用意しているデータフェッチ用Composableです。
データフェッチとは、APIサーバーなどの外部からデータを取得する処理のことです。
普通の非同期処理(fetch や axios をそのまま使うなど)と違い、この2つはNuxtのSSR(SSR:Server Side Renderingの略。サーバー側でHTMLを組み立ててからブラウザに返す仕組み)と連携しています。
サーバーで取得したデータをそのままブラウザ側に引き継いでくれるので、同じAPIリクエストが二重に発生するのを防いでくれます。
なぜ2種類も用意されているのか
useFetch は「URLを渡すだけで手軽に使える」ことを目指したComposableです。
一方の useAsyncData は、その内部で実際に使われている土台のComposableで、より柔軟な非同期処理に対応できます。
つまり useFetch は useAsyncData を使いやすくラップしたものだと考えると理解しやすくなります。
基本の書き方
手順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 を選びます。
比較表で見る違い
| 項目 | useFetch | useAsyncData |
|---|---|---|
| 主な用途 | 単一のAPIをURL指定で取得 | 複数の処理を組み合わせた柔軟な取得 |
| キーの指定 | URL等から自動生成(省略可) | 第1引数で必須(自分で命名) |
| 内部の実体 | useAsyncData + $fetchのラッパー | Nuxtのデータ取得の土台となるComposable |
| 複数リクエストの合成 | 不向き(1リクエスト向け) | 得意(Promise.allなどを内部で自由に書ける) |
| 手軽さ | 高い(URLだけで完結) | やや低い(handlerを自分で書く) |
迷ったときの判断軸はシンプルです。
「1本のAPIをそのまま取得するだけ」なら useFetch、「複数の処理を組み合わせて1つの結果にしたい」なら useAsyncData を選ぶと、遠回りせずに済みます。
SSRでのデータ取得の仕組み
Nuxtアプリを開くと、まずサーバー側でページが描画され、そのときに useFetch や useAsyncData のリクエストも一緒に実行されます。
サーバーで取得したデータは、ペイロード(ペイロード:サーバーからブラウザに渡される初期データのこと)として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.
原因は、useFetch や useAsyncData がVueの ref や computed などと同じ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: false と execute() で制御する形に変えました。
こうすることで、サーバー・ブラウザどちらでも同じ場所で同じキーの useFetch が呼ばれるようになり、警告が出なくなりました。
応用・一歩先の使い方
$fetchとuseFetchの使い分け
もう1つ整理しておきたいのが $fetch との使い分けです。
$fetch はNuxtに組み込まれた素のHTTPクライアントで、SSRとの連携機能を持ちません。
useFetch ・ useAsyncData は「ページを開いたときに表示するための初期データ取得」に向いており、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を内部で使ったラッパーComposableuseAsyncDataは複数の処理を組み合わせられる、より柔軟なComposable- どちらもSSRとブラウザ間でデータを共有し、二重リクエストを防いでくれる
useFetch/useAsyncDataはコンポーネントのトップレベルで呼び出す必要があり、条件分岐で囲むと警告や不整合の原因になる- ページ初期表示のデータ取得には
useFetch/useAsyncData、ユーザー操作起点の通信には$fetchを使い分ける
次に読むべき記事
データの取得方法が分かったら、次は取得先となるAPI自体をNuxtアプリの中に作る方法を知っておくと便利です。
→ 次の記事:server/apiディレクトリでサーバーAPIを作る