Fetching data from an API
Most apps get data from a server through an API — an address that returns JSON. From this lesson on the course switches to the second example — a blog that reads posts from the JSONPlaceholder test API. Loading takes time and can fail, so the component has to show three states: "loading", "error" and the data itself.
A new project
Create the blog project the same way as the task list:
npm create vue@latestName it blog and leave the optional features unchecked for now. Then run cd blog, npm install, npm run dev.
Loading the list of posts
Create src/components/PostList.vue:
<!-- src/components/PostList.vue -->
<script setup>
import { ref, onMounted } from 'vue'
const posts = ref([])
onMounted(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
posts.value = await response.json()
})
</script>
<template>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>Use it in src/App.vue — <PostList /> after the import. A moment after you open http://localhost:5173 the titles of five posts appear. fetch and async/await work just as in the tutorial lessons, and Vue re-renders the list when an array lands in posts.
Loading and error
Let's add two states — loading and error:
<!-- src/components/PostList.vue -->
<script setup>
import { ref, onMounted } from 'vue'
const posts = ref([])
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
if (!response.ok) {
throw new Error(`Server responded ${response.status}`)
}
posts.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>
<template>
<p v-if="loading">Loading...</p>
<p v-else-if="error">Could not load posts: {{ error }}</p>
<ul v-else>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>At first the page shows "Loading...", then the list. To test the error, change posts to postz in the address — you get "Could not load posts: Server responded 404".
Component states
| State | loading | error | What you see |
|---|---|---|---|
| Request in progress | true | null | "Loading..." |
| Error | false | Error text | An error message |
| Success | false | null | The list of posts |
A composable for requests
The same three variables are needed on every page of the blog. Move them into src/composables/useFetch.js:
// src/composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
fetch(url)
.then((response) => {
if (!response.ok) throw new Error(`Server responded ${response.status}`)
return response.json()
})
.then((json) => (data.value = json))
.catch((e) => (error.value = e.message))
.finally(() => (loading.value = false))
return { data, loading, error }
}The component is left with one line: const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5').
Common mistake
Making the whole <script setup> asynchronous by writing await fetch(...) at the top level. Vue renders such a component only inside the special Suspense component, and without it the page stays empty. Load data in onMounted or through a composable, as in this lesson.