Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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@latest

Name 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

StateloadingerrorWhat you see
Request in progresstruenull"Loading..."
ErrorfalseError textAn error message
SuccessfalsenullThe 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.

Thanks for reading!

Donate to the site — we work hard for you.