Vue cookbook · Intermediate

Fetch, refresh and share data with Nuxt useFetch

Pages refetch data in the browser after SSR, and lists don't update after mutations.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Use useFetch with reactive query options so it refetches when filters change, call refresh() after mutations, and share cached data between components with the same key (read it with useNuxtData).

Solution

<!-- app/pages/orders.vue -->
<script setup lang="ts">
const status = ref<"all" | "open" | "shipped">("all");
const page = ref(1);

const { data, pending, error, refresh } = await useFetch("/api/orders", {
  key: "orders",
  query: { status, page },                                // refetches when these change
  transform: (res: { items: { id: string; total: number }[]; pages: number }) => res,
});

async function cancel(id: string) {
  await $fetch(`/api/orders/${id}/cancel`, { method: "POST" });   // $fetch in event handlers is fine
  await refresh();
}
</script>

<template>
  <select v-model="status">
    <option value="all">All</option>
    <option value="open">Open</option>
    <option value="shipped">Shipped</option>
  </select>
  <p v-if="pending">Loading...</p>
  <p v-else-if="error">Couldn't load orders.</p>
  <ul v-else>
    <li v-for="order in data?.items" :key="order.id">
      {{ order.id }}: {{ order.total }}
      <button @click="cancel(order.id)">Cancel</button>
    </li>
  </ul>
</template>

Pitfalls

  • await $fetch() in <script setup>, which runs twice (server and client).
  • Building the URL string from refs (/api/orders?page=${page.value}) instead of the reactive query option, so it doesn't refetch.
  • Reusing a key for different data.

Variations

  • refreshNuxtData("orders") refreshes from anywhere by key.
  • lazy: true renders the page immediately and shows pending while loading client-side.

Further reading: useFetch

Reference: https://nuxt.com/docs/4.x/api/composables/use-fetch

More Vue recipes

Was this page helpful?