404 Not Found

404 Not Found


nginx

データ取得:useFetch

AliceがMegaShopのホームページを開くと, 空白のページが表示されました。商品データはクライアントがリクエストするまで取得されず, レンダリングに3秒かかりました。CharlieはNuxt 3のuseFetchがサーバー側でデータを事前取得できることを知っていました。HTMLに直接商品情報を含めることで, ファーストビューで即座にコンテンツが表示されます。

1. 学ぶ内容


2. 消費者の実話

(1) ペインポイント:3秒間の空白画面

AliceがスマホでMegaShopを開くと, 3秒間スピナーが回る空白画面が表示され, その後ようやく商品が現れました。地下鉄の電波が弱い環境ではさらに悪く, 5秒間空白でした。Charlieが分析したところ, 商品データはクライアント側のJavaScriptが読み込まれるまで取得されないことがわかりました。レンダリングプロセスは「JSダウンロード → 実行 → フェッチ → レンダリング」という順序で, 非常に遅いのです。

(2) useFetch SSR事前取得による解決策

useFetchはSSRフェーズでデータを取得し, HTMLに直接商品コンテンツを含めます:

VUE
<script setup lang="ts">
// サーバー:SSR中にデータを取得
// クライアント:事前取得済みデータを使用, 重複リクエストなし
const { data: products } = await useFetch('/api/products')
</script>

(3) 結果:ファーストビュー読み込み時間が3秒から0.8秒に短縮

SSR事前取得後, Aliceはページを開くとすぐに商品が見えます。LCPは3秒から0.8秒に低下し, ユーザー体験スコアは52から94に改善しました。


3. useFetchとuseAsyncData

(1) SSRデータ事前取得からクライアントハイドレーションまでの流れ

100%
sequenceDiagram
    participant B as ブラウザ
    participant S as Nuxtサーバー
    participant A as APIサーバー

    B->>S: GET /products
    S->>A: fetch /api/products
    A-->>S: JSONデータ
    S->>S: データ付きHTMLをレンダリング
    S-->>B: HTML + payload (データ埋め込み)
    B->>B: HTMLを表示 (即時ファーストペイント)
    B->>B: payloadでハイドレート (再フェッチなし)

(2) useFetchuseAsyncDataの比較

項目 useFetch useAsyncData
目的 $fetchのラッパー 汎用データ取得
リクエスト方法 内蔵$fetch カスタムハンドラ
URL反応性 ✅ URL変更時に自動再リクエスト ❌ 手動watchが必要
型推論 ✅ レスポンス型を自動推論 ⚠️ ジェネリクス型を手動指定
ユースケース ほとんどのAPIリクエスト 非標準リクエスト/複雑ロジック

(1) ▶ サンプル:useFetchの基本使用法

VUE
<!-- pages/products/index.vue -->
<template>
  <div>
    <h1>商品</h1>
    <div v-if="pending">読み込み中...</div>
    <div v-else-if="error">商品の読み込みに失敗しました</div>
    <div v-else>
      <ProductCard v-for="p in products" :key="p.id" :product="p" />
    </div>
  </div>
</template>

<script setup lang="ts">
const { data: products, pending, error } = await useFetch('/api/products')
</script>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:useAsyncDataのカスタムハンドラ

VUE
<script setup lang="ts">
const { data: stats } = await useAsyncData('product-stats', async () => {
  // カスタムデータ取得ロジック
  const [total, featured, onSale] = await Promise.all([
    $fetch('/api/products/count'),
    $fetch('/api/products/featured'),
    $fetch('/api/products/on-sale')
  ])
  return { total, featured, onSale }
})
</script>

出力:

TEXT
// 実行成功

4. $fetchとuseFetch

(1) 主要な違いの比較

項目 $fetch useFetch
SSRデータ引き渡し ❌ 引き渡しなし ✅ 自動payload引き渡し
リアクティブ ❌ 生データ ✅ refリアクティブ
ステータス管理 ❌ なし ✅ pending/error/refresh
重複リクエスト ❌ 重複の可能性 ✅ 重複排除
ユースケース イベントハンドリング/APIルート内 ページデータ取得

(1) ▶ サンプル:$fetchをイベントハンドリングに使用

VUE
<script setup lang="ts">
// $fetchは一回きりのリクエスト用 (SSR payload不要)
async function submitOrder() {
  const order = await $fetch('/api/orders', {
    method: 'POST',
    body: { items: cart.value, total: totalPrice.value }
  })
  navigateTo(`/orders/${order.id}`)
}
</script>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:サーバーAPI内で$fetchを使用

TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
  // サーバーハンドラ内では$fetchが正しい選択
  const query = getQuery(event)
  const products = await $fetch('https://api.supplier.com/products', {
    params: { category: query.category }
  })
  return products
})

出力:

TEXT
// 実行成功

5. リクエストオプションの詳解

(1) 主要オプションクイックリファレンス

オプション デフォルト 説明
server boolean true サーバー側で実行するかどうか
lazy boolean false 遅延読み込みを有効にするか (非ブロッキングナビゲーション)
immediate boolean true 即時実行するかどうか
dedupe string 'cancel' 重複リクエストポリシー
transform function - レスポンスデータを変換
pick array - 指定フィールドのみ抽出
default function - データ読み込み前のデフォルト値

(1) ▶ サンプル:Lazyモード (非ブロッキングナビゲーション)

VUE
<template>
  <div>
    <h1>おすすめ商品</h1>
    <!-- lazy:バックグラウンドでデータを読み込み, ページは即時レンダリング -->
    <div v-if="pending">おすすめを読み込み中...</div>
    <div v-else>
      <ProductCard v-for="p in recommendations" :key="p.id" :product="p" />
    </div>
  </div>
</template>

<script setup lang="ts">
const { data: recommendations, pending } = await useFetch('/api/recommendations', {
  lazy: true
})
</script>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:transformとpick

VUE
<script setup lang="ts">
// Transform:保存前にデータを処理
const { data: products } = await useFetch('/api/products', {
  transform: (data: any[]) => {
    return data.map(p => ({
      ...p,
      formattedPrice: new Intl.NumberFormat('en-US', {
        style: 'currency', currency: 'USD'
      }).format(p.price)
    }))
  }
})

// Pick:特定フィールドのみ抽出 (payloadサイズを削減)
const { data: productNames } = await useFetch('/api/products', {
  pick: ['id', 'name', 'price']
})
</script>

出力:

TEXT
// 実行成功

(3) ▶ サンプル:server: false (クライアント専用取得)

VUE
<script setup lang="ts">
// SSRフェッチをスキップ - クライアント側のみフェッチ
const { data: userWishlist } = await useFetch('/api/wishlist', {
  server: false,
  default: () => [] // データ読み込み前のデフォルト値
})
</script>

出力:

TEXT
// 実行成功

6. リフレッシュとポーリング

(1) リフレッシュ戦略の比較

戦略 アプローチ シナリオ 頻度
手動リフレッシュ refresh() ユーザーアクション後 オンデマンド
Watch自動リフレッシュ watchオプション パラメータ変更時 パラメータ変更時
ポーリング setInterval + refresh リアルタイムデータ 定期実行
リアルタイム WebSocket 超リアルタイム プッシュ

(1) ▶ サンプル:手動リフレッシュ

VUE
<template>
  <div>
    <h1>商品</h1>
    <button @click="refresh()" :disabled="pending">
      {{ pending ? 'リフレッシュ中...' : 'リフレッシュ' }}
    </button>
    <ProductCard v-for="p in products" :key="p.id" :product="p" />
  </div>
</template>

<script setup lang="ts">
const { data: products, pending, refresh } = await useFetch('/api/products')
</script>

出力:

TEXT
// 実行成功

(2) ▶ サンプル:パラメータ値変更時の自動リフレッシュ

VUE
<template>
  <div>
    <select v-model="selectedCategory">
      <option value="all">すべて</option>
      <option value="electronics">エレクトロニクス</option>
      <option value="clothing">衣類</option>
    </select>
    <ProductCard v-for="p in products" :key="p.id" :product="p" />
  </div>
</template>

<script setup lang="ts">
const selectedCategory = ref('all')

// selectedCategoryが変更されると自動再フェッチ
const { data: products } = await useFetch('/api/products', {
  query: { category: selectedCategory },
  watch: [selectedCategory]
})
</script>

出力:

TEXT
// 実行成功

(3) ▶ サンプル:リアルタイム在庫のポーリング

VUE
<script setup lang="ts">
const { data: stock, refresh } = await useFetch('/api/stock/live', {
  server: false // クライアント専用ポーリング
})

// 30秒ごとにポーリング
const pollInterval = setInterval(() => {
  refresh()
}, 30000)

onUnmounted(() => clearInterval(pollInterval))
</script>

出力:

TEXT
// 実行成功

7. 総合例:MegaShop商品一覧SSR事前取得

VUE
<!-- pages/products/index.vue -->
<template>
  <div class="product-list-page">
    <h1>MegaShop商品</h1>

    <!-- フィルター -->
    <div class="filters">
      <select v-model="filters.category" @change="applyFilters">
        <option value="">全カテゴリ</option>
        <option value="electronics">エレクトロニクス</option>
        <option value="clothing">衣類</option>
        <option value="home">ホーム&ガーデン</option>
      </select>
      <select v-model="filters.sort">
        <option value="popular">人気順</option>
        <option value="price-asc">価格:安い順</option>
        <option value="price-desc">価格:高い順</option>
      </select>
    </div>

    <!-- 読み込み中状態 -->
    <div v-if="pending" class="loading">商品を読み込み中...</div>

    <!-- エラー状態 -->
    <div v-else-if="error" class="error">
      商品の読み込みに失敗しました。<button @click="refresh()">再試行</button>
    </div>

    <!-- 商品グリッド -->
    <div v-else class="product-grid">
      <ProductCard
        v-for="p in products"
        :key="p.id"
        :product="p"
        @add-to-cart="addToCart"
      />
    </div>

    <!-- ページネーション -->
    <div class="pagination">
      <button :disabled="page <= 1" @click="page--">前へ</button>
      <span>{{ page }}ページ</span>
      <button @click="page++">次へ</button>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Product {
  id: number; name: string; price: number; image: string
}

const page = ref(1)
const filters = reactive({
  category: '',
  sort: 'popular'
})

// リアクティブクエリ付きSSR事前取得
const { data: products, pending, error, refresh } = await useFetch<Product[]>('/api/products', {
  query: computed(() => ({
    page: page.value,
    category: filters.category || undefined,
    sort: filters.sort
  })),
  default: () => [],
  transform: (data: Product[]) => data.map(p => ({
    ...p,
    formattedPrice: `$${p.price.toLocaleString()} USD`
  }))
})

const cart = useState<Product[]>('cart', () => [])
function addToCart(product: Product) {
  cart.value.push(product)
}

function applyFilters() {
  page.value = 1
  refresh()
}

// ページ変更を監視して自動リフレッシュ
watch(page, () => refresh())
</script>

❓ よくある質問

Q useFetchはクライアントで重複リクエストを引き起こしますか?
A いいえ。Nuxt 3のpayloadメカニズムはSSR中に取得したデータをHTMLにシリアライズし, クライアントはハイドレーション時にそれを直接使用するため, 重複リクエストは発生しません。
Q いつuseFetchの代わりに$fetchを使うべきですか?
A イベントハンドラ (ボタンクリックでフォームを送信する時など)やサーバーAPI内から呼び出す場合は$fetchを使います。ページの初期データ取得にはuseFetchを使います。
Q useFetchのキーはどのように生成されますか?
A デフォルトではURLとリクエストオプションから一意のキーが自動生成されます。手動で制御する必要がある場合 (複数リクエスト間でキャッシュを共有するなど)は, keyパラメータを渡すことができます。
Q lazyserver: falseの違いは何ですか?
A lazyはSSR中もリクエストを実行しますが, ナビゲーションをブロックしません (ページが先にレンダリングされ, データを待ちます)。server: falseはSSRリクエストを完全にスキップし, クライアント側でのみデータを取得します。
Q なぜuseFetchのレスポンスデータはrefであり, 生の値ではないのですか?
A データは変更される可能性があり (refresh/watch), refがリアクティブ性を保証します。分割代入時はtoRefsを使うか, .valueで直接アクセスしてください。
Q dedupeの"cancel"と"defer"オプションの違いは何ですか?
A "cancel" (デフォルト)は前の未完了リクエストをキャンセルして新しいリクエストを開始します。"defer"は前のリクエストの結果を再利用し, 新しいリクエストを開始しません。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度:⭐):useFetchを使って公開API (jsonplaceholderなど)からデータを取得し, データリストを表示し, ソースコードにデータが含まれているか確認してSSRを検証してください。
  2. 応用問題 (難易度:⭐⭐):ページネーション付き商品一覧を実装し, 「次のページ」ボタンクリック時にwatchで自動的にデータを再読み込みするようにしてください。
  3. チャレンジ (難易度:⭐⭐⭐):主要商品のSSR事前取得とおすすめ商品のlazy読み込みを同時に実装し, 2つのアプローチのユーザー体験の違いを比較してください。

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%