Zademy

Nuxt 4:$fetch、useFetch、useAsyncData - データ取得完全ガイド

Nuxt; Vue; Fetching; SSR; DataFetching
3034 単語

Nuxtでのデータ取得の管理は、3つのレベルの階層に整理されており、各ツールには異なるアーキテクチャ上の役割があります:

ツールアーキテクチャ上の役割主な目的SSR状態転送(ハイドレーション)
$fetch低レベルユーティリティ(Raw HTTP Client)純粋なHTTPネットワークリクエストの実行。いいえ。 SSRコンポーネントで直接使用すると、二重fetchingが発生します。
useAsyncData状態プリミティブ(Core Primitive)非同期状態のライフサイクルを管理し、サーバーとクライアント間のデータ転送を保証します。はい。 結果をキャプチャし、Nuxtのpayloadにシリアル化し、二重リクエストを回避します。
useFetch便利なラッパー(Developer Wrapper)最も一般的なユースケースを簡素化:単一のendpoint HTTPからデータを取得。はい。 内部的にuseAsyncDataで動作し、その機能を継承します。

useFetchは本質的に構文糖衣(shortcut)であり、useAsyncData$fetchを一緒に実装します。

いつどれを使用するのが最適か

選択は、操作のアーキテクチャコンテキストに基本的に依存します:

1. useFetch:シンプルな初期データ取得(GET)

単一のendpointからのGETリクエストを介した初期データ取得にはuseFetchを使用することをお勧めします。特に、Server-Side Rendering(SSR)のサポートが必要で、データの二重取得を回避したい場合です。最もクリーンな構文のオプションです。

明確な例: ページの記事リストを取得する。

// app/pages/posts.vue
<script setup lang="ts">
  // リクエストを一度(サーバー上で)実行し、状態を転送します。
  const { data: posts } = await useFetch('/api/posts')
</script>

2. useAsyncData:複雑な非同期ロジックまたは非HTTPデータソース

非同期操作が単純なendpointへのHTTP呼び出しでない場合、useAsyncDataを使用する必要があります。これは、任意の非同期プロミスをラップするための主要なツールです。

明確な例:

  • 並列呼び出し: 複数の$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を使用することは、状態ハイドレーションの恩恵を受けないため、一般的に非効率的です。

    // イベントハンドラー内で使用
    async function submitForm() {
    	await $fetch("/api/contact", {
    		method: "POST",
    		body: formData.value
    	});
    }
  • 内部サーバールートへの呼び出し: NuxtのAPIルート内(例:server/api)で、$fetchはHTTPリクエストを回避し、ハンドラー関数を直接呼び出し、サーバー上の速度を最適化します。

利点と欠点

ツール利点欠点
$fetch内部呼び出しの最適化: 内部サーバールートをHTTPラウンドトリップなしで直接呼び出します(レイテンシの節約)。ミューテーションに最適: クライアントアクションの直接的でシンプルな使用。二重Fetchingのリスク: useAsyncDataなしでコンポーネントで使用すると、クライアントはハイドレーション中にリクエストを再度実行します。
useAsyncData完全な制御: 任意の非同期ロジック(HTTPだけでなく)をラップできます。SSRフレンドリー: データが一度だけ(サーバー上で)取得され、クライアントに転送されることを保証します(double-fetchingの防止)。完全なリアクティブ状態: datapendingerrorstatus(状態を示す文字列:"idle""pending""success""error")を返します。Nuxt 4の最適化: デフォルトのdata値はshallowRef(浅いリアクティビティ)で、大きなデータ構造でCPUオーバーヘッドを削減し、パフォーマンスを向上させます。より冗長: ハンドラー(handler)と一意のキー(key)を明示的に定義する必要があります。
useFetchシンプルな構文(DX): SSRセーフなリクエストを実行する最も簡単な方法。自動keyingメカニズム: URLとオプションに基づいて自動的にキーを生成します。強力な型付け: サーバールートの応答とURL型を推論できます。制限あり: 単一のendpoint HTTP呼び出しにのみ適しています。setupフェーズでのミューテーション(POST、PUT、DELETE)には推奨されません。

クライアント側とサーバー側のどちらから使用するか

ここでの重要な違いは、ユニバーサルレンダリング(SSR)のための状態転送(State Transfer)の必要性であり、「データの二重取得」(double-fetching)を防ぎます。

サーバー側(SSR)とユニバーサルレンダリング

サーバー側レンダリングにNuxtを使用し、初期HTMLにデータが存在する必要がある場合(SEOとパフォーマンスに不可欠)、payloadを介して状態転送を処理する関数を使用する必要があります:

コンテキスト推奨ツール理由
ページ/コンポーネントでの初期データ取得(GET)useFetchまたはuseAsyncDataデータがサーバー上で一度だけ取得され、クライアントでハイドレートされることを保証し、二重リクエストを回避します。
内部APIルートへの呼び出し(サーバーコードから)。$fetchNuxtは呼び出しをインターセプトし、実際のHTTPリクエストを実行せずに直接実行するため、より高速です。

警告: サーバーレンダリングされたコンポーネント(<script setup>)での初期データ取得に$fetchのみを使用すると、クライアントはハイドレーション中にネットワーク呼び出しを再度実行し、二重fetchingになります。

クライアント側(Client-Side)

ハイドレーション後またはユーザーインタラクションに応答して実行される操作は、低レベルのutilityを使用する必要があります:

コンテキスト推奨ツール理由
ユーザーイベントによってトリガーされるミューテーション(POST、PUT、DELETE)$fetchクライアント側の直接HTTP呼び出しを実行するための推奨方法です。状態はサーバーから転送される必要がないため、useAsyncDataoverheadは不要です。
SEOに重要でないまたはクライアント依存のデータ(例:ジオロケーション)。useFetch/useAsyncDataserver: falseこれにより、サーバー上でのハンドラーの実行が無効になり、クライアント側のハイドレーションが完了するまでデータ取得が延期されます。
既存のデータを更新または遅延関数を実行する。refresh()/execute()useFetch/useAsyncDataによって返される)$fetchを使用せずに、データ取得ロジックを手動で再実行できます。

useFetchuseAsyncDataを、サーバーからクライアントに移動する金庫と考えてください:サーバーがデータで満たし、封印します。クライアントに到着すると、クライアントはデータを取得しに行く必要がなく(二重リクエストを回避)、金庫を開けるだけです。一方、$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. 一意のキー戦略

// ✅ キーイングのベストプラクティス
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は、デフォルトでデータに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
		};
	});
}

デバッグと監視

デバッグツール

// 開発中のデバッグモードを有効にする
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 }
				});
			}
		}
	});
}

結論

$fetchuseFetchuseAsyncDataの間の正しい選択は、効率的で保守可能なNuxt 4アプリケーションを構築するために基本的です:

  • $fetch ミューテーションとクライアント側の直接呼び出し用
  • useFetch SSRを使用したシンプルなデータ取得用
  • useAsyncData 複雑な非同期ロジックと状態の完全な制御用

これらの違いを理解することは、二重フェッチングなどの一般的な問題を防ぐだけでなく、Nuxt 4のユニバーサルレンダリング機能を最大限に活用し、より高速で効率的で、より良いユーザーエクスペリエンスを持つアプリケーションを構築することを可能にします。


このガイドは、Nuxt 4の公式ドキュメントとコミュニティによって確立されたベストプラクティスに基づいています。詳細については、Nuxtの公式ドキュメントとNuxtリポジトリの例を参照してください。