- Artigos
- Desenvolvimento
- Dominando o useFetch no Nuxt 3 e 4

Se você já construiu um app Nuxt, provavelmente usou $fetch pelo menos uma vez — e se perguntou por que os dados somem ao recarregar a página, duplicam entre componentes ou aparecem vazios no client após o SSR.
É exatamente esse problema que o useFetch resolve.
useFetch é o composable de data fetching do Nuxt. Ele envolve o $fetch com suporte a SSR, deduplicação de requisições, estado compartilhado e hidratação. Quando você entende como ele funciona por baixo dos panos, ele se torna uma das ferramentas mais poderosas do ecossistema Nuxt.
Este guia cobre o modelo mental, padrões práticos e os casos de borda que costumam aparecer só em produção.
O modelo mental: três trabalhos ao mesmo tempo
Toda chamada a useFetch faz três coisas:
Buscar dados — como o
$fetch, mas integrado ao ciclo de vida do NuxtCompartilhar resultados — a mesma
keyretorna o mesmo estado reativo entre componentesConectar server e client — a saída do SSR é serializada no payload e hidratada no client
Quando uma página renderiza no servidor, o Nuxt executa suas chamadas useFetch, armazena os resultados em nuxtApp.payload.data e embute esse payload no HTML. No client, o Nuxt reutiliza esses dados em vez de disparar requisições duplicadas.
Por isso useFetch('/api/users') em dois componentes irmãos não significa duas chamadas HTTP — desde que compartilhem a mesma key.
useFetch vs $fetch vs useAsyncData
$fetch— requisições pontuais em event handlers, server routes ou utilitáriosuseFetch— requisições HTTP em componentes/páginas com SSR + cacheuseLazyFetch— igual aouseFetch, mas não bloqueia a navegaçãouseAsyncData— qualquer trabalho assíncrono: queries no banco, leitura de arquivos, lógica em múltiplas etapas
useFetch é essencialmente useAsyncData + $fetch conectados:
// Conceitualmente equivalentes:
const { data } = await useFetch('/api/posts')
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'))Use useAsyncData quando o fetch não for uma requisição HTTP simples. Use useFetch quando for.
Seu primeiro fetch
<script setup lang="ts">
const { data, status, error, refresh } = await useFetch('/api/posts')
</script>
<template>
<Loader v-if="status === 'pending'" />
<p v-else-if="error">{{ error.message }}</p>
<PostList v-else :posts="data" />
</template>No Nuxt 4, prefira status em vez do booleano pending mais antigo. Ele oferece estados explícitos: 'idle', 'pending', 'success' e 'error'.
Para dados críticos que precisam existir antes da página renderizar, aguarde o fetch no top level. O Nuxt vai esperar durante o SSR.
Para dados não bloqueantes — widgets, sidebars, conteúdo abaixo da dobra — use useLazyFetch:
const { data: stories } = useLazyFetch('https://api.example.com/stories', {
key: 'featured-stories'
})A navegação segue imediatamente; o componente renderiza com status === 'pending' até os dados chegarem.
Keys: a opção mais importante que você não está definindo
Por padrão, o Nuxt gera uma key a partir da URL e das opções. Isso funciona até você adicionar query params reativos, rotas dinâmicas ou composables compartilhados.
Sempre defina uma key explícita quando:
A URL sozinha não é única o suficiente
Você faz fetch dentro de um composable reutilizável
Quer comportamento de cache previsível entre navegações
// composables/useHackerNews.ts
export function useHackerNews() {
return useLazyFetch('https://hn.algolia.com/api/v1/search', {
query: { tags: 'front_page', hitsPerPage: 10 },
key: 'hacker-news-front-page',
transform: (response) => response.hits.map(toStory)
})
}Sem key: 'hacker-news-front-page', dois componentes chamando esse composable podem obter entradas de cache não relacionadas — ou perder a deduplicação.
Regra prática: se você encapsula useFetch em um composable, defina uma key.
Queries reativas e refetch automático
Passe refs reativas em query, e o Nuxt refaz o fetch quando elas mudam:
<script setup lang="ts">
const route = useRoute()
const pageNumber = computed(() => route.params.page)
const { data: posts, status } = await useFetch('/api/posts', {
query: { page: pageNumber }
})
</script>Quando o usuário navega de /articles/page/1 para /articles/page/2, pageNumber atualiza e o Nuxt busca a próxima página automaticamente. Sem watch manual.
Para outras dependências reativas, use a opção watch:
const category = ref('nuxt')
const { data } = await useFetch('/api/posts', {
query: { category },
watch: [category]
})Transform, pick e default
transform — modele os dados antes de chegar ao template
APIs raramente retornam exatamente o que a UI precisa. Transforme na camada de fetch, não em cada componente:
const { data: stories } = await useLazyFetch(apiUrl, {
transform: (response: ApiResponse) =>
response.hits.filter(hit => hit.title).map(toStory)
})O valor transformado é o que vai para o cache e para a hidratação. Seus componentes ficam limpos.
pick — cacheie só o que você precisa
const { data } = await useFetch('/api/user', {
pick: ['id', 'name', 'avatar']
})Útil quando a API retorna objetos grandes, mas a página precisa de poucos campos.
default — evite null checks em todo lugar
const { data: posts } = await useFetch('/api/posts', {
default: () => []
})data é sempre um array, mesmo antes da requisição completar.
Bloqueante vs lazy: escolhendo o padrão certo
Conteúdo da página (artigo, detalhe de projeto) →
await useFetch(...)— bloqueia até estar prontoConfig bootstrap global do app →
await useFetch(..., { lazy: false })noapp.vueWidgets de sidebar, seções opcionais →
useLazyFetch(...)Ações disparadas pelo usuário →
$fetchdentro de um event handler
No app.vue, dados bootstrap dos quais todas as páginas dependem devem bloquear o SSR:
<script setup lang="ts">
const { data } = await useApi<Bootstrap>('/bootstrap', { lazy: false })
if (data.value) {
bootstrap.value = data.value
}
</script>Todo o resto do app herda esse contexto sem refazer o fetch.
Tratamento de erros que funciona com SSR
Lançar erros dentro de um componente quebra o SSR silenciosamente ou produz renders parciais confusos. Use as utilidades de erro do Nuxt:
const { data: post, error } = await useFetch(`/api/posts/${slug}`)
if (error.value) {
throw createError({
statusCode: error.value.statusCode ?? 404,
statusMessage: 'Post not found'
})
}O Nuxt renderiza a página de erro apropriada no server e no client, com o status HTTP correto.
Para falhas mais leves — widgets opcionais — trate erros no template:
<template>
<aside v-if="error">Não foi possível carregar recomendações.</aside>
<Recommendations v-else :items="data" />
</template>Criando um composable customizado com createUseFetch
Se toda página repete o mesmo baseURL, headers de auth e lógica de cache, extraia um composable global:
// composables/useApi.ts
export const useApi = createUseFetch((callerOptions) => ({
baseURL: useRuntimeConfig().public.apiUrl,
lazy: true,
...callerOptions,
getCachedData: (key, nuxtApp) => {
return nuxtApp.payload.data[key] ?? nuxtApp.static?.data?.[key]
}
}))Agora cada fetch de página é uma linha:
const { data: projects } = await useApi<Project[]>('/projects/featured')
const { data: post } = await useApi<Post>(`/posts/${slug}`)Wrappers finos como usePageFetch mantêm a lógica específica de rota em um só lugar:
export function usePageFetch<T>(slug: string) {
return useApi<Page<T>>(`/pages/${slug}`)
}Esse padrão escala bem: defaults ficam no useApi, semântica de domínio em composables nomeados, páginas legíveis.
Cache, ISR e getCachedData
O Nuxt 4 mudou a reatividade padrão de deep para shallow por performance. Para a maioria dos dados buscados, isso é suficiente — você substitui o objeto inteiro no refetch. Se mutar propriedades aninhadas diretamente, opte por:
const { data } = await useAsyncData('user', () => $fetch('/api/user'), {
deep: true
})Para ISR e geração estática, getCachedData permite reutilizar dados do payload entre navegações em vez de refazer o fetch:
getCachedData: (key, nuxtApp) => {
return nuxtApp.payload.data[key] ?? nuxtApp.static?.data?.[key]
}Combinado com route rules como isr: 300, suas páginas servem HTML em cache e revalidam em background — enquanto navegações client-side continuam instantâneas porque o payload já está quente.
Deduplicação e cancelamento de requisições (Nuxt 4.2+)
Quando usuários disparam refetches rápidos — busca enquanto digita, troca de abas — controle o que acontece com requisições em andamento:
const { data } = await useFetch('/api/search', {
query: { q: searchTerm },
watch: [searchTerm],
dedupe: 'cancel'
})Para controle manual, passe um AbortSignal:
const controller = new AbortController()
const { data, execute } = await useFetch('/api/users', {
immediate: false,
signal: controller.signal
})
await execute()
// depois: controller.abort()Dentro de handlers useAsyncData, o signal está disponível como argumento:
const { data, execute } = await useAsyncData('users',
({ signal }) => $fetch('/api/users', { signal }),
{ immediate: false }
)Armadilhas comuns
1. Usar $fetch no setup sem useFetch
// ❌ Roda no server E no client — fetch duplo, sem estado compartilhado
const posts = await $fetch('/api/posts')
// ✅ Buscado uma vez, hidratado no client
const { data: posts } = await useFetch('/api/posts')2. Esquecer o await em páginas que precisam de dados no SSR
// ❌ Página renderiza antes dos dados chegarem no server
const { data } = useFetch('/api/posts')
// ✅ Nuxt espera o fetch durante o SSR
const { data } = await useFetch('/api/posts')3. URLs de API hardcoded
Use useRuntimeConfig() para base URLs, assim o mesmo código funciona em dev, staging e produção:
const config = useRuntimeConfig()
const { data } = await useFetch('/posts', {
baseURL: config.public.apiUrl
})Ou centralize em um wrapper createUseFetch.
4. Mutar dados buscados sem deep: true
No Nuxt 4, mutações aninhadas em refs shallow não disparam reatividade. Refaça o fetch ou defina deep: true se realmente precisar de mutação in-place.
5. Keys ausentes em composables
Se duas chamadas geram auto-keys diferentes para o mesmo recurso, você perde a deduplicação. Defina keys explícitas.
Cheat sheet de decisão
Precisa buscar em um componente ou página?
├── É uma requisição HTTP simples?
│ ├── Deve bloquear a renderização? → await useFetch()
│ └── Pode carregar após a navegação? → useLazyFetch()
└── Lógica assíncrona customizada (DB, arquivos)? → useAsyncData()
Precisa buscar em um event handler? → $fetch()
Precisa de defaults compartilhados no app? → createUseFetch()
Precisa refazer fetch quando um ref muda? → query: { param: myRef }
Precisa cancelar requisições obsoletas? → dedupe: 'cancel' ou AbortSignalJuntando tudo
Uma página de listagem paginada costuma combinar vários padrões:
<script setup lang="ts">
const route = useRoute()
const pageNumber = computed(() => route.params.page)
const { data: page, status: pageStatus, error: pageError } = usePageFetch('articles')
const { data: posts, status, error } = await useApi<PaginatedResponse<Post>>('/posts', {
query: { page: pageNumber }
})
if (pageError.value || error.value) {
throw createError({
statusCode: pageError.value?.status ?? error.value?.status ?? 500
})
}
</script>
<template>
<Loader v-if="pageStatus === 'pending' || status === 'pending'" />
<PostList v-else-if="page && posts" :posts="posts" :page="page" />
</template>Dois composables, paginação reativa, respostas tipadas, tratamento de erro compatível com SSR — e zero gerenciamento manual de cache.
Considerações finais
useFetch não é só um wrapper conveniente em torno do fetch. É o contrato do Nuxt entre sua camada de dados e o pipeline de renderização: o que é buscado no server, o que viaja no payload, o que é compartilhado entre componentes e o que refaz fetch na navegação.
Quem tem dificuldade com data fetching no Nuxt geralmente trata useFetch como um $fetch reativo. Quem domina pensa em keys, payloads e hidratação — e deixa o Nuxt cuidar do resto.