Recuperação de Dados: useFetch
Quando Alice abriu a homepage da MegaShop, viu uma página em branco—os dados dos produtos só eram buscados quando o cliente fazia a requisição, e levava 3 segundos para renderizar. Charlie sabia que o useFetch do Nuxt 3 podia pré-carregar dados no lado do servidor, permitindo que o HTML incluísse diretamente as informações dos produtos para que o conteúdo ficasse imediatamente visível na primeira tela.
1. O Que Você Vai Aprender
- useFetch / useAsyncData: O Processo Completo de Pré-busca SSR e Hidratação no Cliente
- Diferenças Entre a Requisição Raw $fetch e a Requisição Wrapped useFetch
- Opções de requisição: server/lazy/immediate/dedupe/transform/pick
- Refresh e Polling: refresh() / modo watch
- Lista de Produtos MegaShop: Pré-busca SSR na Prática
2. Uma História Real de uma Consumidora
(1) Dor: Uma tela em branco por 3 segundos
Alice abriu a MegaShop no celular e viu uma tela em branco com um ícone de carregamento por 3 segundos antes dos produtos aparecerem. No metrô com conexão fraca, era pior—5 segundos de tela em branco. Charlie analisou e descobriu que os dados dos produtos só são buscados após o JavaScript do lado do cliente terminar de carregar. Todo o processo segue esta sequência: download do JavaScript → execução → fetch → renderização—o que é lento demais.
(2) Solução com Pré-busca SSR do useFetch
O useFetch busca dados durante a fase de SSR, e o HTML inclui diretamente o conteúdo dos produtos:
<script setup lang="ts">
// Servidor: buscar dados durante o SSR
// Cliente: usa dados pré-buscados, sem requisição duplicada
const { data: products } = await useFetch('/api/products')
</script>
(3) Resultados: Tempo de carregamento da primeira tela reduzido de 3 segundos para 0,8 segundos
Após o pré-carregamento SSR, Alice vê o produto imediatamente ao abrir a página; o LCP caiu de 3 segundos para 0,8 segundos, e a pontuação de experiência do usuário melhorou de 52 para 94.
3. useFetch e useAsyncData
(1) Linha do Tempo da Pré-busca de Dados SSR até a Hidratação no Cliente
sequenceDiagram
participant B as Navegador
participant S as Servidor Nuxt
participant A as Servidor API
B->>S: GET /products
S->>A: fetch /api/products
A-->>S: Dados JSON
S->>S: Renderizar HTML com dados
S-->>B: HTML + payload (dados embutidos)
B->>B: Exibir HTML (primeira pintura instantânea)
B->>B: Hidratar com payload (sem re-fetch)
(2) Comparação de useFetch e useAsyncData
| Dimensão | useFetch | useAsyncData |
|---|---|---|
| Propósito | Wrapper para $fetch | Recuperação geral de dados |
| Método de Requisição | $fetch integrado | Handler Personalizado |
| URL Responsiva | ✅ Re-requisição automática quando a URL muda | ❌ Deve ser observado manualmente |
| Inferência de Tipos | ✅ Infere automaticamente o tipo de resposta | ⚠️ Tipos genéricos devem ser especificados manualmente |
| Caso de Uso | Maioria das Requisições API | Requisições Não-padronizadas/Lógica Complexa |
(1) ▶Exemplo: Uso Básico do useFetch
<!-- pages/products/index.vue -->
<template>
<div>
<h1>Produtos</h1>
<div v-if="pending">Carregando...</div>
<div v-else-if="error">Falha ao carregar produtos</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>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Handler personalizado para useAsyncData
<script setup lang="ts">
const { data: stats } = await useAsyncData('product-stats', async () => {
// Lógica personalizada de busca de dados
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>
Saída:
// Execução Bem-sucedida
4. $fetch vs. useFetch
(1) Comparação das Diferenças Chave
| Dimensão | $fetch | useFetch |
|---|---|---|
| Passagem de Dados SSR | ❌ Sem passagem | ✅ Passagem automática de payload |
| Reatividade | ❌ Dados brutos | ✅ ref Reativo |
| Gerenciamento de Status | ❌ Nenhum | ✅ pending/error/refresh |
| Requisição Duplicada | ❌ Possível Duplicação | ✅ Dedupe |
| Casos de Uso | Tratamento de Eventos/Rota API | Recuperação de Dados da Página |
(1) ▶Exemplo: Usando $fetch para tratamento de eventos
<script setup lang="ts">
// $fetch é para requisições únicas (sem necessidade de payload SSR)
async function submitOrder() {
const order = await $fetch('/api/orders', {
method: 'POST',
body: { items: cart.value, total: totalPrice.value }
})
navigateTo(`/orders/${order.id}`)
}
</script>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Usando $fetch em uma API do servidor
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
// $fetch é a escolha certa dentro de handlers do servidor
const query = getQuery(event)
const products = await $fetch('https://api.supplier.com/products', {
params: { category: query.category }
})
return products
})
Saída:
// Execução Bem-sucedida
5. Explicação Detalhada das Opções de Requisição
(1) Referência Rápida das Opções Chave
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
| server | boolean | true | Se deve executar no servidor |
| lazy | boolean | false | Se deve habilitar lazy loading (navegação não-bloqueante) |
| immediate | boolean | true | Se deve executar imediatamente |
| dedupe | string | 'cancel' | Política de Requisição Duplicada |
| transform | function | - | Transformar dados de resposta |
| pick | array | - | Extrair apenas os campos especificados |
| default | function | - | Valor padrão antes dos dados serem carregados |
(1) ▶Exemplo: Modo lazy (navegação não-bloqueante)
<template>
<div>
<h1>Recomendações de Produtos</h1>
<!-- lazy: dados carregam em background, página renderiza imediatamente -->
<div v-if="pending">Carregando recomendações...</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>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: transform e pick
<script setup lang="ts">
// Transform: processar dados antes de armazenar
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: extrair apenas campos específicos (reduzir tamanho do payload)
const { data: productNames } = await useFetch('/api/products', {
pick: ['id', 'name', 'price']
})
</script>
Saída:
// Execução Bem-sucedida
(3) ▶Exemplo: server: false (busca apenas no cliente)
<script setup lang="ts">
// Pular busca SSR - buscar apenas no lado do cliente
const { data: userWishlist } = await useFetch('/api/wishlist', {
server: false,
default: () => [] // Valor padrão antes dos dados carregarem
})
</script>
Saída:
// Execução Bem-sucedida
6. Refresh e Polling
(1) Comparação de Estratégias de Refresh
| Estratégia | Abordagem | Cenário | Frequência |
|---|---|---|---|
| Refresh Manual | refresh() | Após ação do usuário | Sob demanda |
| Auto-Refresh por Watch | opções watch | quando parâmetros mudam | quando parâmetros mudam |
| Polling | setInterval + refresh | Dados em tempo real | Agendado |
| Tempo real | WebSocket | Ultra-tempo real | Push |
(1) ▶Exemplo: Refresh Manual
<template>
<div>
<h1>Produtos</h1>
<button @click="refresh()" :disabled="pending">
{{ pending ? 'Atualizando...' : 'Atualizar' }}
</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>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Atualização Automática Quando os Valores dos Parâmetros Mudam
<template>
<div>
<select v-model="selectedCategory">
<option value="all">Todos</option>
<option value="electronics">Eletrônicos</option>
<option value="clothing">Roupas</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 quando selectedCategory muda
const { data: products } = await useFetch('/api/products', {
query: { category: selectedCategory },
watch: [selectedCategory]
})
</script>
Saída:
// Execução Bem-sucedida
(3) ▶Exemplo: Polling de Estoque em Tempo Real
<script setup lang="ts">
const { data: stock, refresh } = await useFetch('/api/stock/live', {
server: false // Polling apenas no cliente
})
// Polling a cada 30 segundos
const pollInterval = setInterval(() => {
refresh()
}, 30000)
onUnmounted(() => clearInterval(pollInterval))
</script>
Saída:
// Execução Bem-sucedida
7. Exemplo Completo: Pré-busca SSR da Lista de Produtos MegaShop
<!-- pages/products/index.vue -->
<template>
<div class="product-list-page">
<h1>Produtos MegaShop</h1>
<!-- Filtros -->
<div class="filters">
<select v-model="filters.category" @change="applyFilters">
<option value="">Todas as Categorias</option>
<option value="electronics">Eletrônicos</option>
<option value="clothing">Roupas</option>
<option value="home">Casa e Jardim</option>
</select>
<select v-model="filters.sort">
<option value="popular">Mais Populares</option>
<option value="price-asc">Preço: Menor para Maior</option>
<option value="price-desc">Preço: Maior para Menor</option>
</select>
</div>
<!-- Estado de carregamento -->
<div v-if="pending" class="loading">Carregando produtos...</div>
<!-- Estado de erro -->
<div v-else-if="error" class="error">
Falha ao carregar produtos. <button @click="refresh()">Tentar Novamente</button>
</div>
<!-- Grid de produtos -->
<div v-else class="product-grid">
<ProductCard
v-for="p in products"
:key="p.id"
:product="p"
@add-to-cart="addToCart"
/>
</div>
<!-- Paginação -->
<div class="pagination">
<button :disabled="page <= 1" @click="page--">Anterior</button>
<span>Página {{ page }}</span>
<button @click="page++">Próxima</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'
})
// Pré-busca SSR com query reativa
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()
}
// Observar mudança de página para auto-refresh
watch(page, () => refresh())
</script>
❓Perguntas Frequentes
useFetch faz o cliente fazer requisições duplicadas?useFetch é gerada?key.lazy e server: false?lazy ainda executa a requisição durante o SSR, mas não bloqueia a navegação (a página renderiza primeiro e depois espera os dados). server: false pula completamente a requisição SSR e busca os dados apenas no lado do cliente.useFetch são um ref em vez de um valor bruto?ref garante reatividade. Ao desestruturar, use toRefs ou acesse diretamente usando .value.dedupe?📖Resumo
- useFetch = pré-busca SSR + hidratação no cliente + estado reativo; é o método preferido para buscar dados da página
- $fetch é uma ferramenta de requisição bruta, adequada para tratamento de eventos e chamadas internas da API do servidor
- "lazy" para navegação não-bloqueante, "server: false" para pular SSR, e "transform/pick" para otimizar dados
- refresh() atualiza manualmente; a opção watch rastreia automaticamente mudanças nos parâmetros
- MegaShop usa
useFetchewatchpara implementar pré-busca SSR para listas de produtos e sincronização de filtros
📝Exercícios
- Exercício Básico (Dificuldade: ⭐): Use
useFetchpara obter dados de uma API pública (como jsonplaceholder), exiba a lista de dados e verifique se o SSR está funcionando conferindo o código-fonte para ver se os dados estão incluídos. - Problema Avançado (Dificuldade ⭐⭐): Implemente uma lista de produtos com paginação que recarrega automaticamente os dados usando
watchquando o botão "Próxima Página" é clicado. - Desafio (Dificuldade: ⭐⭐⭐): Implemente simultaneamente a pré-busca SSR de produtos core e o lazy loading de produtos recomendados, e compare as diferenças na experiência do usuário entre as duas abordagens.
---|



