Fetching Data — Vue

Composable Pattern const data = ref(null) const loading = ref(true) const error = ref(null) onMounted(async () => { try { const res = await…

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([]), lalu onMounted(() => { 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

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>

📌 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 })

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari