Nuxt 4:$fetch、useFetch、useAsyncData - データ取得完全ガイド
Nuxt のデータ取得には $fetch、useAsyncData、useFetch の 3 つのレイヤーがあります。それぞれアーキテクチャ上の役割が違うので、混同すると二重リクエストや無駄なオーバーヘッドに悩まされます。ここでは私なりの使い分けを整理しておきます。
$fetch は低レベルな HTTP クライアントです。純粋なネットワークリクエストを実行するだけで、SSR の状態転送は関知しません。SSR コンポーネント内で直接使うと、クライアント側で同じリクエストがもう一度飛んでしまいます。
useAsyncData は状態プリミティブです。非同期状態のライフサイクルを管理し、サーバーとクライアント間のデータ転送を保証します。結果を Nuxt の payload にシリアライズして、二重リクエストを防ぎます。
useFetch は useAsyncData と $fetch を組み合わせた便利なラッパーです。要するに構文糖衣で、よくある「単一のエンドポイントから GET で取ってくる」ケースをシンプルに書けます。
いつどれを使用するのが最適か
選択は、その操作がどのコンテキストで実行されるかに依存します。
1. useFetch:シンプルな初期データ取得(GET)
単一のエンドポイントからの GET リクエストで、SSR サポートが必要な場合は useFetch を使います。一番シンプルに書けます。
例えば、記事リストを取得するページならこれで十分です。
// app/pages/posts.vue
<script setup lang="ts">
// リクエストを一度(サーバー上で)実行し、状態を転送します。
const { data: posts } = await useFetch('/api/posts')
</script>2. useAsyncData:複雑な非同期ロジックまたは非HTTPデータソース
非同期処理が単純な HTTP 呼び出しでない場合、useAsyncData が主役になります。任意の非同期 Promise をラップできるのが強みです。
複数の $fetch を並列で投げて結果をまとめる場合:
const { data } = await useAsyncData("dashboard", async () => {
const [user, stats] = await Promise.all([$fetch("/api/user"), $fetch("/api/stats")]);
return { user, stats };
});GraphQL クライアントやデータベース ORM など、$fetch を使わないクライアントを統合する場合:
// myGetFunctionはデータクライアントのカスタム関数です
const { data } = await useAsyncData("users", () => myGetFunction("users"));3. $fetch:ミューテーションとクライアント/サーバー駆動のアクション
ユーザーイベント(クリック、フォーム送信など)で駆動されるクライアント側の操作には $fetch を使います。SSR の状態同期が不要な低レベル呼び出しにも適しています。
フォーム送信のようなミューテーション(POST/PUT/DELETE)は useFetch ではなく $fetch で行うべきです。ミューテーションに useFetch を使っても状態ハイドレーションの恩恵を受けられず、無駄が生じます。
// イベントハンドラー内で使用
async function submitForm() {
await $fetch("/api/contact", {
method: "POST",
body: formData.value
});
}Nuxt の内部サーバールート(server/api 以下)の呼び出しでも $fetch が有利です。HTTP ラウンドトリップを回避してハンドラー関数を直接呼び出すので、サーバー上で速くなります。
利点と欠点
$fetch の強みは内部呼び出しの最適化とミューテーションでのシンプルさです。ただし useAsyncData なしでコンポーネント内に書くと、ハイドレーション時にクライアント側で再度リクエストが飛んでしまいます。
useAsyncData は任意の非同期ロジックをラップでき、SSR フレンドリーで、data、pending、error、status("idle"、"pending"、"success"、"error")を返します。Nuxt 4 ではデフォルトで data が shallowRef になり、大きなデータ構造でも CPU オーバーヘッドを抑えられます。代償は、handler と一意の key を明示的に書く必要があることです。
useFetch は SSR セーフなリクエストを一番シンプルに書けて、URL とオプションから自動的にキーを生成し、サーバールートの型も推論してくれます。ただし単一のエンドポイント呼び出しにしか適しておらず、setup 内でのミューテーションには向きません。
クライアント側とサーバー側のどちらから使用するか
ここで肝になるのは、ユニバーサルレンダリング(SSR)における状態転送(State Transfer)です。要するに、サーバーで取ったデータをクライアントに渡して、二重取得を防ぐ仕組みのことです。
サーバー側(SSR)とユニバーサルレンダリング
サーバー側レンダリングで初期 HTML にデータを含める必要がある場合(SEO やパフォーマンスのため)、payload を通じた状態転送を扱う関数を使わなければなりません。
ページやコンポーネントでの初期データ取得(GET)には useFetch か useAsyncData を使います。これにより、データがサーバーで一度だけ取得され、クライアントでハイドレートされます。
一方、サーバーコードから内部 API ルートを呼ぶ場合は $fetch が速いです。Nuxt が呼び出しをインターセプトして、実際の HTTP リクエストを飛ばさずに直接実行するからです。
サーバーレンダリングされたコンポーネントの setup で $fetch だけを使うと、クライアント側でハイドレーション時に再度ネットワーク呼び出しが走ります。これが二重フェッチの正体です。
クライアント側(Client-Side)
ハイドレーション後やユーザー操作に応答する処理では $fetch が基本です。
ユーザーイベントでトリガーされるミューテーション(POST、PUT、DELETE)には $fetch を使います。状態をサーバーから転送する必要がないので、useAsyncData のオーバーヘッドは不要です。
SEO に重要でないデータやクライアント依存のデータ(ジオロケーションなど)を取得する場合は、useFetch / useAsyncData に server: false を指定します。サーバー側での実行が無効になり、クライアント側のハイドレーション完了後にデータ取得が始まります。
既存データの更新には useFetch / useAsyncData が返す refresh() や execute() を使います。$fetch を直接呼ぶよりも、元の取得ロジックを再実行できるので安全です。
useFetch と useAsyncData を、サーバーからクライアントに渡す金庫だと考えてください。サーバーがデータで満たして封印し、クライアントに届いたら封を開けるだけで済みます。もう一度取りに行く必要はありません。一方 $fetch は単なる電話です。サーバーがかけても、クライアントは自分のコピーを得るためにもう一度かけ直さなければなりません。
高度な実用例
useAsyncDataを使用した最適化されたロードパターン
// pages/dashboard.vue
<script setup lang="ts">
// 詳細な状態管理を備えた最適化されたロード
const {
data: dashboard,
pending,
error,
refresh
} = await useAsyncData('dashboard', async () => {
// より良いパフォーマンスのための並列呼び出し
const [user, stats, notifications] = await Promise.all([
$fetch('/api/user/profile'),
$fetch('/api/analytics/stats'),
$fetch('/api/notifications/unread')
])
return {
user,
stats,
notifications
}
}, {
// 高度なオプション
server: true, // サーバー上で実行
lazy: false, // レンダリング前に待機
default: () => ({ // ロード中のデフォルト値
user: null,
stats: { views: 0, users: 0 },
notifications: []
})
})
// デバウンスを使用した手動更新
const debouncedRefresh = useDebounceFn(refresh, 300)
// 自動更新のためのWatch
watch(() => route.path, debouncedRefresh)
</script>$fetchを使用したミューテーションとエラー処理
// components/ContactForm.vue
<script setup lang="ts">
const formData = ref({
name: '',
email: '',
message: ''
})
const isSubmitting = ref(false)
const submitError = ref<string | null>(null)
async function submitForm() {
isSubmitting.value = true
submitError.value = null
try {
const response = await $fetch('/api/contact', {
method: 'POST',
body: formData.value,
// 追加オプション
timeout: 10000,
retry: 2,
retryDelay: 1000
})
// 成功処理
await navigateTo('/thank-you')
} catch (error) {
if (error.data?.message) {
submitError.value = error.data.message
} else {
submitError.value = 'フォームの送信中にエラーが発生しました。もう一度お試しください。'
}
} finally {
isSubmitting.value = false
}
}
</script>useFetchを使用したデータ変換
// pages/products/[id].vue
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
description: string
category: string
}
interface ProductWithDetails extends Product {
formattedPrice: string
isInStock: boolean
relatedProducts: Product[]
}
const { data: product, pending } = await useFetch<ProductWithDetails>(
`/api/products/${route.params.id}`,
{
// データ変換
transform: (data: Product) => ({
...data,
formattedPrice: new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(data.price),
isInStock: data.price > 0,
relatedProducts: [] // 後で埋められます
}),
// 高度なキャッシング
server: true,
key: `product-${route.params.id}`,
// キャッシュオプション
getCachedData: (key) => {
// カスタムキャッシュロジック
return nuxtApp.staticData[key] || nuxtApp.payload.data[key]
}
}
)
// 関連製品を後でロード
watchEffect(async () => {
if (product.value?.category) {
const { data: related } = await $fetch<Product[]>(`/api/products/related/${product.value.category}`)
product.value.relatedProducts = related
}
})
</script>ベストプラクティスとパターン
1. 一意のキー戦略
useAsyncData のキーはユニークで意味のあるものにします。複数パラメータに依存するデータは、すべてパラメータを含めないとキャッシュが衝突します。
// ✅ キーイングのベストプラクティス
const { data } = await useAsyncData(`user-${userId}`, () => $fetch(`/api/users/${userId}`));
// 複数のパラメータに依存するデータの場合
const { data } = await useAsyncData(`search-${searchTerm}-${page}-${category}`, () =>
$fetch("/api/search", {
query: { q: searchTerm, page, category }
})
);2. ロード状態の処理
// components/DataLoader.vue
<script setup lang="ts">
interface AsyncDataState<T> {
data: Ref<T | null>
pending: Ref<boolean>
error: Ref<Error | null>
refresh: () => Promise<void>
}
// 再利用可能なコンポーザブル
function useAsyncDataWithState<T>(
key: string,
handler: () => Promise<T>
): AsyncDataState<T> {
const { data, pending, error, refresh } = useAsyncData(key, handler)
return {
data,
pending,
error,
refresh
}
}
// コンポーネントでの使用
const { data: posts, pending, error } = useAsyncDataWithState(
'posts',
() => $fetch('/api/posts')
)
</script>
<template>
<div>
<div v-if="pending" class="loading">
投稿を読み込んでいます...
</div>
<div v-else-if="error" class="error">
エラー: {{ error.message }}
<button @click="refresh">再試行</button>
</div>
<div v-else-if="data" class="posts">
<PostCard
v-for="post in data"
:key="post.id"
:post="post"
/>
</div>
</div>
</template>3. useLazyFetchを使用したネットワーク最適化
// 後でロードできる重要でないデータの場合
const { data: comments } = await useLazyFetch(`/api/posts/${postId}/comments`, {
server: false, // クライアントのみ
lazy: true // レンダリングをブロックしない
});
// 頻繁に更新されるデータの場合
const { data: liveScore } = await useFetch("/api/live-score", {
server: false,
refresh: {
// 30秒ごとに更新
interval: 30000
}
});パフォーマンスに関する考慮事項
1. Nuxt 4のShallow Refs
Nuxt 4 はデフォルトで data に shallowRef を使います。大きなデータ構造でも深い再帰監視のコストを避けられるので、ほとんどのケースではこのままで十分です。
// Nuxt 4では、これはデフォルトでshallowRefです
const { data } = await useAsyncData("large-dataset", () => $fetch("/api/large-dataset"));
// 深いリアクティビティが必要な場合(慎重に使用)
const { data } = await useAsyncData("deep-reactive", () => $fetch("/api/nested-data"), {
deep: true // 深いリアクティビティを有効にする
});2. キャッシュ戦略
// コンポーネントレベルのキャッシュ
const { data } = await useFetch("/api/config", {
key: "app-config",
// 5分間キャッシュ
server: true,
transform: data => {
// データはキャッシュされたままになります
return data;
}
});
// 条件付きキャッシュ
const { data } = await useFetch("/api/user-preferences", {
key: `user-prefs-${userId}`,
server: false,
// エラーがない場合のみキャッシュ
getCachedData: key => {
const cached = nuxtApp.staticData[key] || nuxtApp.payload.data[key];
return cached && !cached.error ? cached : null;
}
});TypeScriptとの統合
useFetchの強力な型付け
// types/api.ts
export interface User {
id: number
name: string
email: string
avatar?: string
}
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
// pages/users/[id].vue
<script setup lang="ts">
const route = useRoute()
const { data: user } = await useFetch<ApiResponse<User>>(`/api/users/${route.params.id}`)
// 型安全なアクセス
if (user.value?.data) {
console.log(user.value.data.name) // TypeScriptは型を知っています
}
</script>useAsyncDataの型付け
// composables/useDashboard.ts
export interface DashboardData {
user: User;
stats: {
views: number;
clicks: number;
conversions: number;
};
recentActivity: Activity[];
}
export function useDashboardData() {
return useAsyncData<DashboardData>("dashboard", async () => {
const [userResponse, statsResponse, activityResponse] = await Promise.all([$fetch<ApiResponse<User>>("/api/user/profile"), $fetch<ApiResponse<Stats>>("/api/analytics/stats"), $fetch<ApiResponse<Activity[]>>("/api/activity/recent")]);
return {
user: userResponse.data,
stats: statsResponse.data,
recentActivity: activityResponse.data
};
});
}デバッグと監視
デバッグツール
開発中は onRequestError、onResponseError、onResponse コールバックでリクエストの詳細をログに出せます。
// 開発中のデバッグモードを有効にする
const { data } = await useFetch("/api/debug-example", {
// コンソールにデバッグ情報を表示
onRequestError({ request, error }) {
console.error("Request error:", { request, error });
},
onResponseError({ response }) {
console.error("Response error:", response.status, response.statusText);
},
onResponse({ response }) {
console.log("Response received:", response._data);
}
});パフォーマンス監視
// パフォーマンスを測定するコンポーザブル
function useTrackedFetch<T>(url: string, options = {}) {
const startTime = Date.now();
return useFetch<T>(url, {
...options,
onResponse() {
const duration = Date.now() - startTime;
console.log(`${url}へのフェッチに${duration}msかかりました`);
// 遅い場合は分析に送信
if (duration > 1000) {
$fetch("/api/analytics/slow-request", {
method: "POST",
body: { url, duration }
});
}
}
});
}結論
3 つの違いを一言でまとめます。ミューテーションとクライアント側の直接呼び出しには $fetch。SSR を使ったシンプルなデータ取得には useFetch。複雑な非同期ロジックや状態の完全な制御には useAsyncData。
この使い分けを押さえておけば、二重フェッチングの罠を避けつつ、Nuxt 4 のユニバーサルレンダリングを最大限に活用できます。詳しくは Nuxt の公式ドキュメント を参照してください。