Fetching data artinya meminta data ke server, misalnya daftar menu warung, lalu menampilkannya. Permintaan itu disebut request, jawabannya response. Karena butuh waktu dan bisa gagal, komponennya memegang tiga keadaan: sedang memuat (loading), gagal (error), dan berhasil (data). Bayangin memesan di Warung Bu Sri. Setelah kamu pesan, pelayan bilang "sedang dimasak" (loading). Lalu dia datang membawa pesananmu (data), atau kembali bilang "maaf, habis" (error). Pelayan yang baik gak pernah membiarkanmu menatap meja kosong tanpa kabar.
🧪 Tebakan cepat:
const menu = ref([]), laluonMounted(() => { menu.value = fetch("/api/menu") }). Template-nya<p>{{ menu }}</p>. Apa yang tampil: daftar menunya, atau sesuatu yang lain? Tebak dulu, jawabannya ada di bagian "fetch mengembalikan Promise".
Tiga state: loading, error, data
const menu = ref([])
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await fetch("/api/menu")
if (!res.ok) throw new Error("HTTP " + res.status)
menu.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
---template---
<p v-if="loading">Memuat menu...</p>
<p v-else-if="error">Gagal: {{ error }}</p>
<ul v-else>
<li v-for="m in menu" :key="m.id">{{ m.nama }} · Rp{{ m.harga }}</li>
</ul>
| Waktu | loading |
error |
menu |
Tampil |
|---|---|---|---|---|
| komponen dipasang | true | null | [] | Memuat menu... |
| 0,8 detik, server menjawab 200 | false | null | 3 menu | Nasi Goreng · Rp15000, Mie Ayam · Rp12000, Es Teh · Rp5000 |
| atau server menjawab 500 | false | "HTTP 500" | [] | Gagal: HTTP 500 |
onMountedjalan sesudah komponen tampil di layar, tempat yang lazim untuk memulai request. Callback-nya bolehasync.await fetch(url)menunggu response datang.await res.json()menunggu body-nya selesai dibaca lalu mengubahnya jadi object. Dua-duanya butuhawait.tryberisi pekerjaannya,catchmenangkap kegagalan, danfinallyselalu jalan, berhasil atau gagal. Jadiloadingpasti kembalifalse.v-if,v-else-if,v-elsememilih tepat satu dari tiga tampilan.- Di
<script setup>sungguhan, import dulu:import { ref, onMounted } from "vue". Di editor latihan sudah tersedia.
fetch mengembalikan Promise
fetch() gak mengembalikan data. Dia mengembalikan Promise: janji bahwa hasilnya datang nanti. Tanpa await, menu.value berisi Promise itu, bukan daftar menu. Template menampilkannya sebagai teks "[object Promise]", dan menu.length jadi undefined. Itu jawaban tebakannya. Tulis await di dua tempat: const res = await fetch(...) lalu menu.value = await res.json(), di dalam fungsi async.
await di luar fungsi async gak boleh di editor latihan: script-nya dijalankan sebagai badan fungsi biasa, jadi browser berhenti dengan SyntaxError: await is only valid in async functions and the top level bodies of modules. Taruh await di dalam callback onMounted(async () => { ... }).
fetch gak gagal untuk 404 atau 500
fetch cuma gagal (masuk catch) kalau request-nya gak bisa terjadi sama sekali, misalnya internet putus. Response 404 atau 500 tetap response. Bedanya ada di res.ok: true untuk status 200 sampai 299, false selain itu. Tanpa baris if (!res.ok) throw new Error(...), body error seperti { "pesan": "Server sibuk" } masuk ke menu, dan halamannya tampil rusak tanpa pesan apa pun.
Di editor latihan
Latihan gak memakai server sungguhan. Latihan kedua memakai server palsu, ambilData(), yang menjawab seperti fetch sesudah 0,05 detik:
const items = ref([])
const loading = ref(true)
const error = ref(null)
const ambilData = () =>
new Promise((selesai) => {
setTimeout(() => selesai({
ok: true,
status: 200,
json: () => Promise.resolve(["Data 1", "Data 2", "Data 3"]),
}), 50)
})
onMounted(async () => {
try {
const res = await ambilData()
if (!res.ok) throw new Error("HTTP " + res.status)
items.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
---template---
<div>
<p v-if="loading" id="loading">Memuat...</p>
<p v-else-if="error" id="error">Gagal: {{ error }}</p>
<ul v-else id="data">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
- Latihan pertama:
loadingmulaitrue,onMountedmengubahnya jadifalse, dan<p id="status">berbunyiData siap!(sebelumnyaMemuat...). - Latihan kedua di atas:
ambilData()dan template-nya sudah ada di starter, dan kamu menulisonMounted-nya. Pemeriksanya membaca tiga butir dariv-for,Data 1paling atas danData 3paling bawah, lalu mencariawait ambilData(,res.ok,catch, danfinallydi kodemu.itemstetap mulai dariref([]). - Mau mencoba
fetchsungguhan di Playground? Lab di halaman ini membuka versi dengan server palsu: sebuah fungsifetchbuatan yang menjawab denganResponsesesudah jeda.
📌 Mengambil ulang saat id berubah
Halaman detail mengambil /api/menu/1, lalu pembeli memilih menu 2. Request baru dikirim lewat watch. Masalahnya, response gak selalu datang berurutan:
| Waktu | Kejadian | Yang tampil tanpa penjaga |
|---|---|---|
| 0 | pilih Nasi Goreng, request /api/menu/1 (lambat) |
Memuat menu... |
| 0,2 detik | pilih Mie Ayam, request /api/menu/2 (cepat) |
Memuat menu... |
| 0,5 detik | response menu 2 tiba | Mie Ayam |
| 1,5 detik | response menu 1 tiba terakhir | Nasi Goreng, padahal yang dipilih Mie Ayam |
watch(id, async (baru, lama, onCleanup) => {
let kedaluwarsa = false
onCleanup(() => { kedaluwarsa = true })
loading.value = true
error.value = null
try {
const res = await fetch("/api/menu/" + baru)
if (!res.ok) throw new Error("HTTP " + res.status)
const data = await res.json()
if (!kedaluwarsa) menu.value = data
} catch (e) {
if (!kedaluwarsa) error.value = e.message
} finally {
if (!kedaluwarsa) loading.value = false
}
}, { immediate: true })
onCleanupdijalankan Vue tepat sebelum callback berikutnya, jadi request lama ditandai kedaluwarsa begituidberubah, dan hasilnya diabaikan.- Untuk benar-benar membatalkan request di jaringan, pakai
AbortController: kirim{ signal: controller.signal }kefetchdan panggilcontroller.abort()dionCleanup.fetchyang dibatalkan gagal dengan error bernamaAbortError. awaitlangsung di<script setup>(di luar fungsi) membuat komponennya async. Komponen async cuma tampil di dalam<Suspense>; tanpa itu dia gak dirender, dan build development menulissetup function returned a promise, but no <Suspense> boundary was found in the parent component tree.Untuk pemula,onMounteddengan state loading lebih aman.
⚠️ Jebakan yang sering ketemu
- Lupa
await.data.value = fetch(...)menyimpan Promise, bukan data. Tunggufetchdanres.json(). - Gak memeriksa
res.ok. 404 dan 500 gak masukcatch. Lempar error sendiri kalau!res.ok. loadinggak dimatikan saat gagal. Taruhloading.value = falsedifinally, bukan cuma di jalur sukses.- Response lama menimpa yang baru. Tandai request lama lewat
onCleanup, atau batalkan denganAbortController. - Mengisi data sebagai nilai awal. Latihan memeriksa ada
onMounted(yang menulis.value =(atau.push() di dalamnya, dan daftarnya dibuat denganv-for.
TL;DR: Data dari server diambil dengan fetch di dalam onMounted(async () => { ... }), dan komponennya memegang tiga state: loading, error, dan data, dipilih dengan v-if / v-else-if / v-else. fetch mengembalikan Promise, jadi butuh await fetch(...) lalu await res.json(). fetch gak gagal untuk 404 atau 500: periksa res.ok. Matikan loading di finally. Kalau request diulang saat id berubah, abaikan response lama lewat onCleanup atau batalkan dengan AbortController, karena response bisa tiba gak berurutan.