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
- useFetch / useAsyncData: The Complete Process of SSR Prefetching and Client-Side Hydration
- Differences Between the Raw $fetch Request and the Wrapped useFetch Request
- Request options: server/lazy/immediate/dedupe/transform/pick
- Refresh and Polling: refresh() / watch mode
- MegaShop Product List: SSR Pre-fetching in Practice
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:
<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
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
<!-- 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:
// Execution Successful
(2) ▶ Example: Custom handler for useAsyncData
<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:
// 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
<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:
// Execution Successful
(2) ▶ Example: Using $fetch in a server API
// 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:
// 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)
<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:
// Execution Successful
(2) ▶ Example: transform and pick
<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:
// Execution Successful
(3) ▶ Example: server: false (client-only retrieval)
<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:
// 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
<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:
// Execution Successful
(2) ▶ Example: Automatically Refresh When Parameter Values Change
<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:
// Execution Successful
(3) ▶ Example: Polling Real-Time Inventory
<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:
// Execution Successful
7. Comprehensive Example: MegaShop Product List SSR Prefetching
<!-- 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
useFetch cause the client to make duplicate requests?useFetch generated?key parameter.lazy and server: false?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.useFetch a ref rather than a raw value?ref ensures reactivity. When destructuring, use toRefs or access it directly using .value.dedupe?📖 Summary
- useFetch = SSR pre-fetching + client-side hydration + reactive state; it is the preferred method for fetching page data
- $fetch is a raw request tool, suitable for event handling and internal server API calls
- "lazy" for non-blocking navigation, "server: false" to skip SSR, and "transform/pick" to optimize data
- refresh() manually refreshes; the watch option automatically tracks changes in the parameters
- MegaShop uses
useFetchandwatchto implement SSR pre-fetching for product lists and filter synchronization
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Use
useFetchto 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. - Advanced Problem (Difficulty ⭐⭐): Implement a product list with pagination that automatically reloads the data using
watchwhen the "Next Page" button is clicked. - 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.
---|



