Fetching Data — React

Hampir semua app React modern butuh ngambil data dari server: profil user, list produk, chat messages, notifikasi. Pattern dasarnya useEffect + fetch — tapi…

Data dari server gak langsung ada. Komponen tampil dulu, permintaan (request) dikirim ke server, lalu jawabannya datang beberapa saat kemudian, atau malah gagal. Mirip pesan makanan lewat aplikasi: setelah pesan, layarnya bilang "pesanan diproses", lalu makanannya datang, atau muncul "pesanan gagal". Komponen yang mengambil data juga harus siap dengan tiga keadaan itu: loading (menunggu), data (berhasil), dan error (gagal).

🧪 Tebakan cepat: kamu klik Andi, jawabannya lambat (2,4 detik). Sebelum datang, kamu klik Budi, jawabannya cepat (0,8 detik). Kalau kodenya gak dijaga, nama siapa yang tampil di akhir? Tebak dulu, jawabannya ada di bagian "Ganti user dengan cepat".

Tiga keadaan: loading, error, data

Komponen yang mengambil data biasanya menyimpan tiga state:

State Nilai awal Artinya
loading true request masih jalan, jawabannya belum datang
error null pesan kalau gagal, null kalau gak ada masalah
user null datanya, terisi setelah jawaban datang

loading dimulai dari true, karena di render pertama datanya memang belum ada. Kalau dimulai dari false, komponen langsung mencoba menampilkan user.name padahal user masih null, dan halamannya error.

Ambil data di useEffect

Mengambil data adalah side effect: pekerjaan yang menyentuh dunia di luar komponen. Tempatnya di useEffect, bukan langsung di badan komponen. Badan komponen jalan di tiap render, jadi fetch di sana berarti request baru di tiap render.

function UserCard({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    setLoading(true)
    setError(null)

    async function load() {
      try {
        const res = await fetch(`/api/users/${userId}`)
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const data = await res.json()
        setUser(data)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    load()
  }, [userId])

  if (loading) return <p>Memuat...</p>
  if (error) return <p>Gagal memuat: {error}</p>
  return <p>{user.name}, {user.city}</p>
}

Ini urutannya di Lab untuk userId = 2, dengan jaringan normal (500 ms):

Waktu Yang terjadi loading error user Tampil
0 ms render pertama true null null Memuat...
0 ms effect jalan, request ke /api/users/2 dikirim true null null Memuat...
500 ms jawaban datang, setUser lalu setLoading(false) false null Budi Budi, Bandung

Nyalakan "Server error" di Lab: server menjawab 500, res.ok jadi false, kodenya melempar error, dan yang tampil Gagal memuat: HTTP 500.

Kenapa callback useEffect gak boleh async

// ❌ salah
useEffect(async () => {
  const data = await fetchData()
  setData(data)
}, [])

Function async selalu mengembalikan Promise. Padahal callback useEffect cuma boleh mengembalikan dua hal: tidak ada apa-apa, atau function cleanup. React akan menganggap Promise itu cleanup dan memberi peringatan. Jadi buat function async di dalam effect, lalu panggil, seperti load() tadi.

Ganti user dengan cepat

Masalah ini sudah kamu kenal di materi useEffect lanjutan: jawaban request lama yang datang telat bisa menimpa yang baru. Klik Andi (2,4 detik), lalu Budi (0,8 detik):

Waktu Kejadian Tampil
0 s klik Andi, request ke /api/users/1 dikirim Memuat...
0,2 s klik Budi, request ke /api/users/2 dikirim Memuat...
1,0 s jawaban Budi datang Budi, Bandung
2,4 s jawaban Andi datang telat, setUser jalan Andi, Jakarta

Yang dipilih Budi, yang tampil Andi. Itu jawaban tebakannya. Penjaganya satu variabel ignore dan cleanup:

useEffect(() => {
  let ignore = false
  setLoading(true)
  setError(null)

  async function load() {
    try {
      const res = await fetch(`/api/users/${userId}`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const data = await res.json()
      if (!ignore) setUser(data)
    } catch (err) {
      if (!ignore) setError(err.message)
    } finally {
      if (!ignore) setLoading(false)
    }
  }

  load()
  return () => { ignore = true }
}, [userId])

📌 Di app sungguhan: TanStack Query

Pola tadi wajib kamu pahami, tapi menulisnya di tiap komponen melelahkan, dan masih banyak yang belum diurus: cache (data yang sudah diambil disimpan, jadi gak perlu minta lagi), coba ulang kalau gagal, ambil ulang saat tab dibuka lagi. Di app sungguhan, orang biasanya memakai library data seperti TanStack Query. BelajarWeb.dev juga memakainya.

async function getUser(userId) {
  const res = await fetch(`/api/users/${userId}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

function UserCard({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => getUser(userId),
  })

  if (isLoading) return <p>Memuat...</p>
  if (error) return <p>Gagal memuat: {error.message}</p>
  return <p>{user.name}, {user.city}</p>
}

Isinya tetap tiga keadaan yang sama, cuma useEffect, ignore dan tiga useState-nya diurus library. queryKey adalah nama data itu di cache, dan library-nya cuma menampilkan jawaban untuk key yang sedang dipakai, jadi masalah jawaban telat tadi ikut beres. Supaya jalan, app-nya dibungkus QueryClientProvider sekali di bagian paling atas.

⚠️ Jebakan yang sering ketemu

TL;DR: Data dari server butuh tiga state: loading (mulai true), error (mulai null), dan datanya. Ambil di useEffect, dengan [] untuk sekali atau [userId] untuk tiap id berubah. Callback useEffect gak boleh async: buat function async di dalamnya lalu panggil. Cek res.ok karena 404 dan 500 gak dianggap gagal oleh fetch, baca isinya dengan res.json(), dan matikan loading di finally. Kalau id bisa berganti cepat, pasang let ignore = false plus cleanup ignore = true supaya jawaban telat gak menimpa. Di app sungguhan, library seperti TanStack Query mengurus semua ini plus cache.

Yang akan kamu pelajari