---
title: "Data Fetching"
description: "Fetch data from the server route using Nuxt's useFetch composable, handle loading states, and compare the approach to React Server Components and client-side fetching."
canonical_url: "https://vercel.com/academy/nuxt-on-vercel/data-fetching"
md_url: "https://vercel.com/academy/nuxt-on-vercel/data-fetching.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-10-16T17:45:50.069Z"
content_type: "lesson"
course: "nuxt-on-vercel"
course_title: "Nuxt on Vercel"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Data Fetching

# Data Fetching

In Next.js, data fetching splits into two worlds. Server Components fetch data with plain `async/await` at the top of the component. Client Components use libraries like SWR or TanStack Query, or roll their own `useEffect` + `useState` pattern. You choose your world when you choose your component type.

Nuxt's `useFetch` composable works in pages and components. During SSR, it fetches data on the server and serializes the result into the payload. During client navigation, it calls the API from the browser. The same call covers both contexts.

You'll use it to load the 17 hot springs on the browse page.

## Outcome

Load and display hot springs on the browse page using `useFetch`.

## Fast Track

1. Add `useFetch("/api/springs")` to the browse page
2. Loop over the results with `v-for` and render `SpringCard` for each
3. Add a loading state

## Hands-on exercise 2.2

Connect the browse page to the server route and render the springs data.

**Requirements:**

1. Use `useFetch` to load data from `/api/springs` in `app/pages/springs/index.vue`
2. Display a loading message while data is being fetched
3. Render a `SpringCard` for each spring using `v-for`
4. Show an empty state when no springs are returned
5. Display the count of springs found

**Implementation hints:**

- `useFetch` returns `{ data, status, error }`. `status` can be `"idle"`, `"pending"`, `"success"`, or `"error"`
- `v-for` is Vue's loop directive. It replaces `.map()` in JSX: `<div v-for="item in items" :key="item.id">`
- `v-if` / `v-else-if` / `v-else` handle conditional rendering. They replace ternaries and `&&` in JSX
- `SpringCard` is auto-imported from `app/components/`. Pass the spring with `:spring="spring"`

Here's the React version you're probably used to:

```tsx
// Next.js client component: for comparison
"use client";
import { useEffect, useState } from "react";

export default function SpringsPage() {
  const [springs, setSprings] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/springs")
      .then((res) => res.json())
      .then(setSprings)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading...</p>;
  return springs.map((s) => <SpringCard key={s.id} spring={s} />);
}
```

The React version manages state and the request lifecycle in the component. Compare it with the Nuxt version:

```vue title="app/pages/springs/index.vue"
<script setup lang="ts">
const { data: springs, status } = await useFetch("/api/springs");
</script>
```

`useFetch` handles the request, caching, SSR serialization, and reactive updates. The `data` ref changes when the response arrives, and `status` reports the request lifecycle.

The template uses `v-if` and `v-for` to handle the three states: loading, results, and empty:

```vue title="app/pages/springs/index.vue"
<template>
  <div>
    <div>
      <h1>
        Browse Hot Springs
      </h1>
      <p>
        {{ springs?.length ?? 0 }} springs found
      </p>
    </div>

    <!-- Loading state -->
    <div v-if="status === 'pending'">
      Loading springs...
    </div>

    <!-- Results -->
    <div v-else-if="springs?.length">
      <SpringCard v-for="spring in springs" :key="spring.id" :spring="spring" />
    </div>

    <!-- Empty state -->
    <div v-else>
      No springs match your filters. Try broadening your search.
    </div>
  </div>
</template>
```

The template introduces a few Vue conventions:

`v-for="spring in springs"` replaces `springs.map((spring) => ...)`. Put `:key` on the element with `v-for`. The right side accepts JavaScript expressions, but use `computed` for substantial transformations so Vue can cache the result until its dependencies change.

`v-if` / `v-else-if` / `v-else` must be on adjacent sibling elements. If you put a `<div>` between `v-if` and `v-else`, Vue won't connect them. This catches people coming from JSX where ternaries can span any distance.

The `{{ springs?.length ?? 0 }}` expression works because Vue template expressions support most JavaScript. Optional chaining, nullish coalescing, ternaries, method calls. What they don't support: statements. No `if`, no `for`, no variable declarations inside `{{ }}`.

\*\*Note: useFetch vs $fetch\*\*

`useFetch` is for components. It integrates with SSR, deduplicates requests, and returns reactive refs. `$fetch` is for imperative calls: event handlers, utility functions, anywhere you'd use plain `fetch()`. We'll use `$fetch` later when we build favorites and reviews.

\*\*Warning: Don't destructure the data ref\*\*

`const { data: springs } = useFetch(...)` gives you a ref. Access it as `springs.value` in script, or plain `springs` in the template. If you destructure deeper (`const { data: { value: springs } }`), you'll lose reactivity and the template won't update.

## Try It

Start the dev server and visit `http://localhost:3000/springs`. You should see:

1. "Browse Hot Springs" heading with "17 springs found"
2. A two-column grid of spring cards
3. Each card shows the name, truncated description, location, temperature, and a type badge
4. Clicking a card navigates to `/springs/[id]` (still a placeholder page)

Refresh the page. The springs should appear instantly because `useFetch` runs during SSR and the data is embedded in the HTML payload. Open your browser's Network tab and you won't see a separate API call on the initial page load.

Navigate away and return. Client-side navigation triggers a fresh API call, so it appears in the Network tab even though the page uses the same composable.

## Commit

```bash
git add -A && git commit -m "feat(browse): wire up browse page with useFetch and SpringCard"
```

## Done-When

- [ ] The browse page loads and displays all 17 hot springs in a grid
- [ ] Loading state shows "Loading springs..." briefly on client-side navigation
- [ ] Each spring renders as a `SpringCard` with name, description, location, and type
- [ ] You can explain the difference between `useFetch` and `$fetch`

## Solution

```vue title="app/pages/springs/index.vue"
<script setup lang="ts">
const { data: springs, status } = useFetch("/api/springs");
</script>

<template>
  <div>
    <div>
      <h1>
        Browse Hot Springs
      </h1>
      <p>
        {{ springs?.length ?? 0 }} springs found
      </p>
    </div>

    <!-- Loading state -->
    <div v-if="status === 'pending'">
      Loading springs...
    </div>

    <!-- Results -->
    <div v-else-if="springs?.length">
      <SpringCard v-for="spring in springs" :key="spring.id" :spring="spring" />
    </div>

    <!-- Empty state -->
    <div v-else>
      No springs match your filters. Try broadening your search.
    </div>
  </div>
</template>
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
