Fix: Component Already Mounted - Use $fetch Instead in Nuxt

Error message:
[nuxt] [useAsyncData] Component is already mounted, please use $fetch instead.
data fetching 2025-01-25

What Causes This Warning?

This warning appears when you call useAsyncData or useFetch after the component has already mounted. These composables are designed to be called during the setup phase, not after.

The Problem

<script setup>
const loadData = async () => {
  // ❌ Wrong - component is already mounted when button is clicked
  const { data } = await useFetch('/api/data')
}
</script>

<template>
  <button @click="loadData">Load Data</button>
</template>

The Fix

Use $fetch for on-demand data fetching:

<script setup>
const data = ref(null)
const loading = ref(false)

const loadData = async () => {
  loading.value = true
  try {
    // ✅ Correct - use $fetch for post-mount requests
    data.value = await $fetch('/api/data')
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <button @click="loadData" :disabled="loading">
    {{ loading ? 'Loading...' : 'Load Data' }}
  </button>
  <div v-if="data">{{ data }}</div>
</template>

Understanding the Difference

useFetch / useAsyncData

  • Run during component setup (before mount)
  • Participate in SSR (data fetched on server)
  • Cached and deduplicated
  • Used for initial page data

$fetch

  • Can be called anytime
  • Client-side only when called post-mount
  • Not automatically cached
  • Used for on-demand requests

Common Scenarios

Form Submission

<script setup>
const formData = ref({ name: '', email: '' })
const result = ref(null)

const submitForm = async () => {
  // ✅ Use $fetch for form submission
  result.value = await $fetch('/api/submit', {
    method: 'POST',
    body: formData.value
  })
}
</script>

Pagination / Load More

<script setup>
// Initial data with useFetch
const { data: initialData } = await useFetch('/api/items?page=1')
const items = ref(initialData.value?.items || [])
const page = ref(1)

const loadMore = async () => {
  page.value++
  // ✅ Use $fetch for subsequent pages
  const newData = await $fetch(`/api/items?page=${page.value}`)
  items.value.push(...newData.items)
}
</script>

Search / Filter

<script setup>
const searchQuery = ref('')
const results = ref([])

// Debounced search function
const search = useDebounceFn(async () => {
  if (!searchQuery.value) {
    results.value = []
    return
  }
  // ✅ Use $fetch for search requests
  results.value = await $fetch('/api/search', {
    params: { q: searchQuery.value }
  })
}, 300)

watch(searchQuery, search)
</script>

Refresh Data

<script setup>
// Initial fetch
const { data, refresh } = await useFetch('/api/data')

// For manual refresh, use the refresh function from useFetch
const handleRefresh = () => {
  refresh()  // ✅ This is the proper way to refresh
}

// Or if you need to fetch with different params
const fetchWithParams = async (newParams) => {
  // ✅ Use $fetch for dynamic requests
  return await $fetch('/api/data', { params: newParams })
}
</script>

Conditional Fetching

<script setup>
const userId = ref(null)
const userData = ref(null)

// When user selects an ID
const selectUser = async (id) => {
  userId.value = id
  // ✅ Use $fetch for conditional data loading
  userData.value = await $fetch(`/api/users/${id}`)
}
</script>

Error Handling with $fetch

<script setup>
const data = ref(null)
const error = ref(null)

const loadData = async () => {
  try {
    data.value = await $fetch('/api/data')
    error.value = null
  } catch (e) {
    error.value = e.message
    data.value = null
  }
}
</script>

When to Use Each

ScenarioUse
Initial page datauseFetch / useAsyncData
SSR requireduseFetch / useAsyncData
Button click fetch$fetch
Form submission$fetch
Pagination$fetch
Search/filter$fetch
WebSocket data$fetch
Refresh existing datarefresh() from useFetch

Reactive $fetch with watch

If you need reactive fetching like useFetch, combine $fetch with watch:

<script setup>
const userId = ref(1)
const userData = ref(null)

// Watch for changes and fetch
watch(userId, async (newId) => {
  userData.value = await $fetch(`/api/users/${newId}`)
}, { immediate: true })
</script>

Or create a composable:

// composables/useReactiveFetch.ts
export function useReactiveFetch<T>(url: Ref<string> | (() => string)) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  const fetch = async () => {
    loading.value = true
    error.value = null
    try {
      const urlValue = typeof url === 'function' ? url() : url.value
      data.value = await $fetch(urlValue)
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  watch(url, fetch, { immediate: true })

  return { data, loading, error, refresh: fetch }
}

Quick Reference

// Setup phase (before mount)
const { data } = await useFetch('/api/data')  // ✅

// After mount (event handlers, watchers, etc.)
const data = await $fetch('/api/data')  // ✅
const { data } = await useFetch('/api/data')  // ⚠️ Warning