This episode covers fetching data in Vue applications: using fetch and Axios, managing loading state and error handling, fetching patterns with the Composition API and composables, plus integrating Vue Query (TanStack Query) for caching and server state synchronization.

Almost every real application fetches data from a server: product lists, user profiles, notifications. This is where the interface becomes asynchronous — and the most common mistake isn't in the data logic, but in how loading and errors are handled.
Episode 8 covers fetching data with fetch and Axios, organizing loading state and error handling, designing fetching patterns with the Composition API, then introducing Vue Query (TanStack Query) for caching and server state synchronization. After this episode, you'll be able to build UIs that load data confidently and elegantly.
fetch is available in all modern browsers without any extra dependency:
async function ambilProduk() {
const res = await fetch("/api/produk");
if (!res.ok) {
throw new Error("Gagal memuat produk");
}
return res.json();
}fetch("/api/produk") returns a promise; res.ok indicates a successful HTTP response, and res.json() parses the body into a JavaScript object. Always check res.ok because fetch doesn't throw an error on 4xx/5xx statuses.
Axios offers interceptors, timeouts, and automatic JSON conversion:
npm install axiosimport axios from "axios";
const { data } = await axios.get("/api/produk", { timeout: 5000 });axios.get("/api/produk", { timeout: 5000 }) throws an error automatically on any status other than 2xx and parses JSON straight into data. Use Axios when you need interceptors for auth headers or logging — we'll use it in episodes 12 and 13.
Every fetching operation has three states: loading, success, and error. Manage all three explicitly:
<script setup>
import { ref } from "vue";
const data = ref(null);
const loading = ref(true);
const error = ref(null);
async function muat() {
loading.value = true;
error.value = null;
try {
const res = await fetch("/api/produk");
if (!res.ok) throw new Error("Server error");
data.value = await res.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
muat();
</script>
<template>
<div v-if="loading">Memuat data...</div>
<div v-else-if="error">Terjadi error: {{ error }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.nama }}</li>
</ul>
</template>v-if="loading", v-else-if="error", and v-else present the three states without confusion. finally guarantees loading is always turned off, even on error.
To make fetching logic reusable, wrap it into a useFetch composable:
import { ref } from "vue";
export function useFetch(url) {
const data = ref(null);
const loading = ref(true);
const error = ref(null);
async function jalankan() {
try {
const res = await fetch(url);
data.value = await res.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
jalankan();
return { data, loading, error, jalankan };
}useFetch("/api/produk") starts fetching immediately when called in setup and returns three refs plus a reload function. Components just consume the result — this pattern is the foundation for TanStack Query integration.
Vue Query, now called TanStack Query, manages server state with caching, retries, and automatic invalidation:
npm install @tanstack/vue-queryimport { useQuery } from "@tanstack/vue-query";
const { data, isPending, isError, error, refetch } = useQuery({
queryKey: ["produk"],
queryFn: () => fetch("/api/produk").then((r) => r.json()),
});useQuery({ queryKey: ["produk"], queryFn: ... }) fetches the data, stores it in the cache under the key ["produk"], and automatically provides isPending, isError, error, and refetch. The same query isn't refetched unnecessarily — that's the caching you get for free.
When data changes, tell TanStack Query to fetch again:
import { useQueryClient } from "@tanstack/vue-query";
const queryClient = useQueryClient();
async function simpan(data) {
await fetch("/api/produk", { method: "POST", body: JSON.stringify(data) });
queryClient.invalidateQueries({ queryKey: ["produk"] });
}queryClient.invalidateQueries({ queryKey: ["produk"] }) marks the old cache stale and triggers a refetch in every component using that query. This replaces manual synchronization between components — we'll go deeper in episode 14.
Tip
Use TanStack Query for all state that comes from the server, and keep genuinely UI-local state like an open modal or a form inside refs and composables.
Episode 8 equipped you with correct data fetching: fetch and Axios as HTTP clients, the three-state pattern for loading and error handling, a useFetch composable for reusability, and TanStack Query for caching and server state invalidation.
Key takeaways:
res.ok when using fetch.finally ensures loading is turned off no matter the outcome.useFetch composable makes fetching reusable.In the next episode 9, we'll cover routing and navigation — Vue Router for organizing routes and components, dynamic and nested routes with lazy loading, scroll behavior and programmatic navigation, plus route meta for per-route authentication.