404 Not Found

404 Not Found


nginx

Data Retrieval: useFetch

When Alice opened the MegaShop homepage, she saw a blank page—the product data wasn't fetched until the client made a request, and it took 3 seconds to render. Charlie knew that Nuxt 3's useFetch could preload data on the server side, allowing the HTML to directly include product information so that the content was immediately visible on the first screen.

1. What You'll Learn


2. A True Story from a Consumer

(1) Pain Point: A blank screen for 3 seconds

Alice opened MegaShop on her phone and saw a blank screen with a spinning icon for 3 seconds before the products appeared. It was even worse on the subway with a weak network connection—a blank screen for 5 seconds. Charlie analyzed the situation and found that product data isn't fetched until the client-side JavaScript has finished loading. The entire rendering process follows this sequence: JavaScript download → execution → fetch → rendering—which is far too slow.

(2) Solution for useFetch SSR Prefetching

useFetch fetches data during the SSR phase, and the HTML directly includes the product content:

VUE
<script setup lang="ts">
// Server: fetch data during SSR
// Client: use pre-fetched data, no duplicate request
const { data: products } = await useFetch('/api/products')
</script>

(3) Results: First-screen load time reduced from 3 seconds to 0.8 seconds

After SSR preloading, Alice sees the product immediately upon opening the page; LCP dropped from 3 seconds to 0.8 seconds, and the user experience score improved from 52 to 94.


3. useFetch and useAsyncData

(1) SSR Data Prefetching to Client Hydration Timeline

100%
sequenceDiagram
    participant B as Browser
    participant S as Nuxt Server
    participant A as API Server

    B->>S: GET /products
    S->>A: fetch /api/products
    A-->>S: JSON data
    S->>S: Render HTML with data
    S-->>B: HTML + payload (data embedded)
    B->>B: Display HTML (instant first paint)
    B->>B: Hydrate with payload (no re-fetch)

(2) Comparison of useFetch and useAsyncData

Dimension useFetch useAsyncData
Purpose Wrapper for $fetch General data retrieval
Request Method Built-in $fetch Custom Handler
URL Responsive ✅ Automatically re-requests when the URL changes ❌ Must be manually watched
Type Inference ✅ Automatically infers the response type ⚠️ Generic types must be specified manually
Use Case Most API Requests Non-standard Requests/Complex Logic

(1) ▶ Example: Basic Usage of useFetch

VUE
<!-- pages/products/index.vue -->
<template>
  <div>
    <h1>Products</h1>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Failed to load products</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>

Output:

TEXT
// Execution Successful

(2) ▶ Example: Custom handler for useAsyncData

VUE
<script setup lang="ts">
const { data: stats } = await useAsyncData('product-stats', async () => {
  // Custom data fetching logic
  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>

Output:

TEXT
// Execution Successful

4. $fetch vs. useFetch

(1) Comparison of Key Differences

Dimension $fetch useFetch
SSR Data Pass-Through ❌ No pass-through ✅ Automatic payload pass-through
Reactive ❌ Raw Data ✅ ref Reactive
Status Management ❌ None ✅ pending/error/refresh
Duplicate Request ❌ Possible Duplicate ✅ Deduped
Use Cases Event Handling/API Routing Page Data Retrieval

(1) ▶ Example: Using $fetch for event handling

VUE
<script setup lang="ts">
// $fetch is for one-off requests (no SSR payload needed)
async function submitOrder() {
  const order = await $fetch('/api/orders', {
    method: 'POST',
    body: { items: cart.value, total: totalPrice.value }
  })
  navigateTo(`/orders/${order.id}`)
}
</script>

Output:

TEXT
// Execution Successful

(2) ▶ Example: Using $fetch in a server API

TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
  // $fetch is the right choice inside server handlers
  const query = getQuery(event)
  const products = await $fetch('https://api.supplier.com/products', {
    params: { category: query.category }
  })
  return products
})

Output:

TEXT
// Execution Successful

5. Detailed Explanation of Request Options

(1) Quick Reference for Key Options

Option Type Default Description
server boolean true Whether to execute on the server
lazy boolean false Whether to enable lazy loading (non-blocking navigation)
immediate boolean true Whether to execute immediately
dedupe string 'cancel' Duplicate Request Policy
transform function - Transform response data
pick array - Extract only the specified fields
default function - Default value before data is loaded

(1) ▶ Example: Lazy mode (non-blocking navigation)

VUE
<template>
  <div>
    <h1>Product Recommendations</h1>
    <!-- lazy: data loads in background, page renders immediately -->
    <div v-if="pending">Loading recommendations...</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>

Output:

TEXT
// Execution Successful

(2) ▶ Example: transform and pick

VUE
<script setup lang="ts">
// Transform: process data before storing
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: only extract specific fields (reduce payload size)
const { data: productNames } = await useFetch('/api/products', {
  pick: ['id', 'name', 'price']
})
</script>

Output:

TEXT
// Execution Successful

(3) ▶ Example: server: false (client-only retrieval)

VUE
<script setup lang="ts">
// Skip SSR fetching - only fetch on client side
const { data: userWishlist } = await useFetch('/api/wishlist', {
  server: false,
  default: () => [] // Default value before data loads
})
</script>

Output:

TEXT
// Execution Successful

6. Refresh and Polling

(1) Comparison of Refresh Strategies

Strategy Approach Scenario Frequency
Manual Refresh refresh() After user action On demand
Watch Auto-Refresh watch options when parameters change when parameters change
Polling setInterval + refresh Real-time data Scheduled
Real-time WebSocket Ultra-real-time Push

(1) ▶ Example: Manual Refresh

VUE
<template>
  <div>
    <h1>Products</h1>
    <button @click="refresh()" :disabled="pending">
      {{ pending ? 'Refreshing...' : 'Refresh' }}
    </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>

Output:

TEXT
// Execution Successful

(2) ▶ Example: Automatically Refresh When Parameter Values Change

VUE
<template>
  <div>
    <select v-model="selectedCategory">
      <option value="all">All</option>
      <option value="electronics">Electronics</option>
      <option value="clothing">Clothing</option>
    </select>
    <ProductCard v-for="p in products" :key="p.id" :product="p" />
  </div>
</template>

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

// Auto-refetch when selectedCategory changes
const { data: products } = await useFetch('/api/products', {
  query: { category: selectedCategory },
  watch: [selectedCategory]
})
</script>

Output:

TEXT
// Execution Successful

(3) ▶ Example: Polling Real-Time Inventory

VUE
<script setup lang="ts">
const { data: stock, refresh } = await useFetch('/api/stock/live', {
  server: false // Client-only polling
})

// Poll every 30 seconds
const pollInterval = setInterval(() => {
  refresh()
}, 30000)

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

Output:

TEXT
// Execution Successful

7. Comprehensive Example: MegaShop Product List SSR Prefetching

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

    <!-- Filters -->
    <div class="filters">
      <select v-model="filters.category" @change="applyFilters">
        <option value="">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
        <option value="home">Home & Garden</option>
      </select>
      <select v-model="filters.sort">
        <option value="popular">Most Popular</option>
        <option value="price-asc">Price: Low to High</option>
        <option value="price-desc">Price: High to Low</option>
      </select>
    </div>

    <!-- Loading state -->
    <div v-if="pending" class="loading">Loading products...</div>

    <!-- Error state -->
    <div v-else-if="error" class="error">
      Failed to load products. <button @click="refresh()">Retry</button>
    </div>

    <!-- Product grid -->
    <div v-else class="product-grid">
      <ProductCard
        v-for="p in products"
        :key="p.id"
        :product="p"
        @add-to-cart="addToCart"
      />
    </div>

    <!-- Pagination -->
    <div class="pagination">
      <button :disabled="page <= 1" @click="page--">Previous</button>
      <span>Page {{ page }}</span>
      <button @click="page++">Next</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 pre-fetch with reactive query
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 change for auto-refresh
watch(page, () => refresh())
</script>

❓ FAQ

Q Does useFetch cause the client to make duplicate requests?
A No. Nuxt 3's payload mechanism serializes the data fetched during SSR into the HTML, which the client uses directly during hydration, so no duplicate requests are made.
Q When should you use $fetch instead of useFetch?
A Use $fetch in event handlers (such as when clicking a button to submit a form) or when calling it from within a server API. Use useFetch to fetch initial data for the page.
Q How is the key for useFetch generated?
A By default, a unique key is automatically generated using the URL and request options. If you need to control this manually (e.g., to share the cache across multiple requests), you can pass a key parameter.
Q What is the difference between lazy and server: false?
A lazy still executes the request during SSR, but it does not block navigation (the page renders first and then waits for the data). server: false completely skips the SSR request and fetches the data only on the client side.
Q Why is the response data from useFetch a ref rather than a raw value?
A Because the data may change (refresh/watch), and a ref ensures reactivity. When destructuring, use toRefs or access it directly using .value.
Q What is the difference between the "cancel" and "defer" options for dedupe?
A "cancel" (the default) cancels the previous unfinished request and initiates a new one. "defer" reuses the result of the previous request and does not initiate a new one.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Use useFetch to retrieve data from a public API (such as jsonplaceholder), display the data list, and verify that SSR is working by checking the source code to see if the data is included.
  2. Advanced Problem (Difficulty ⭐⭐): Implement a product list with pagination that automatically reloads the data using watch when the "Next Page" button is clicked.
  3. Challenge (Difficulty: ⭐⭐⭐): Implement both SSR pre-fetching of core products and lazy loading of recommended products simultaneously, and compare the differences in user experience between the two approaches.

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏