On this page
Async and API Integration
Most Vue apps load data from APIs. Handle async operations with lifecycle hooks, composables, and clear loading/error states.
Fetch on Mount
<script setup>
import { ref, onMounted } from 'vue';
const posts = ref([]);
const loading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
posts.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
});
</script>
<template>
<p v-if="loading">Loading posts...</p>
<p v-else-if="error" class="error">{{ error }}</p>
<ul v-else>
<li v-for="post in posts.slice(0, 10)" :key="post.id">
{{ post.title }}
</li>
</ul>
</template>
Reactive Fetch with watch
Refetch when route params or filters change:
<script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const user = ref(null);
const loading = ref(false);
watch(
() => route.params.id,
async (id) => {
if (!id) return;
loading.value = true;
user.value = null;
try {
const res = await fetch(`/api/users/${id}`);
user.value = await res.json();
} finally {
loading.value = false;
}
},
{ immediate: true }
);
</script>
useFetch Composable
Extract fetch logic for reuse:
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
watchEffect(async (onCleanup) => {
const endpoint = toValue(url);
loading.value = true;
error.value = null;
data.value = null;
const controller = new AbortController();
onCleanup(() => controller.abort());
try {
const res = await fetch(endpoint, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (err) {
if (err.name !== 'AbortError') error.value = err.message;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
<script setup>
import { useFetch } from '../composables/useFetch';
const { data: posts, error, loading } = useFetch(
'https://jsonplaceholder.typicode.com/posts'
);
</script>
Best Practices
- Always handle loading, error, and empty states in the UI
- Abort requests on unmount or when dependencies change
- Avoid fetching inside
computed— usewatchor composables - Consider TanStack Query for Vue for caching and deduplication in larger apps
Next: test your components with Vitest and Vue Test Utils.