Menu

Fetch Data di React dengan useEffect: Loading, Error, Race

Ambil data di React dengan memulai request di useEffect, menyimpan loading, error, dan data di state, serta mengabaikan respons yang datang setelah input berubah. Membahas async/await di effect, AbortController, race condition, dan kapan sebaiknya memakai library.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Untuk mengambil data di React, mulai request di dalam useEffect, simpan responsnya di state, dan render pesan loading sampai datanya datang. Taruh nilai yang menjadi dependensi request, seperti sebuah id, di array dependensi agar effect mengambil data lagi saat nilai itu berubah.

Contoh di halaman ini tidak bisa mengakses jaringan, jadi fetchUser adalah API palsu: promise yang selesai setelah jeda tertentu, seperti fetch. Ganti 800 menjadi 3000 untuk melihat teks loading tampil lebih lama.

Dengan API sungguhan, isi effect-nya terlihat seperti ini:

useEffect(() => {
    fetch('https://api.example.com/users/1')
        .then((res) => res.json())
        .then((data) => setUser(data));
}, []);

Loading, error, dan data

Sebuah request punya tiga hasil yang harus ditampilkan layar: masih loading, gagal, atau selesai. Simpan masing-masing di state, dan reset semuanya saat effect memulai request baru.

Klik User 3: API palsu itu menolak, pesan error menggantikan nama, dan konsol menampilkan error-nya. Blok finally menghapus loading di kedua jalur, jadi request yang gagal tidak pernah membiarkan spinner terus menyala.

Saat fetch berkomunikasi dengan server sungguhan, respons 404 atau 500 tidak membuat promise ditolak. Periksa res.ok dan lempar error sendiri:

const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

async/await di dalam effect

Fungsi yang kamu kirim ke useEffect tidak boleh async. React mengharapkannya tidak mengembalikan apa pun atau mengembalikan fungsi cleanup, sedangkan fungsi async selalu mengembalikan promise.

// Wrong: the effect returns a promise
useEffect(async () => {
    const data = await fetchUser(id);
    setUser(data);
}, [id]);

// Right: define an async function inside and call it
useEffect(() => {
    async function load() {
        const data = await fetchUser(id);
        setUser(data);
    }
    load();
}, [id]);

Race condition

Saat id berubah dengan cepat, ada dua request yang berjalan bersamaan. Tidak ada jaminan keduanya menjawab sesuai urutan. Jika request yang lebih lama lebih lambat, ia datang paling akhir dan menimpa data untuk id yang paling baru dipilih pengguna.

Solusinya adalah flag ignore. Setiap kali effect berjalan, ia punya flag sendiri, dan cleanup-nya mengatur flag itu ke true. Karena React menjalankan cleanup sebelum effect berjalan lagi, respons dari jalannya effect yang sudah usang melihat ignore === true dan dibuang. Blok ini merender profil yang sama dua kali, tanpa dan dengan flag:

Klik tombolnya lalu tunggu. Jawaban cepat dari user 2 datang lebih dulu, jadi kedua profil mencatat log dan menampilkan Grace. Sekitar satu setengah detik setelah klik, jawaban lambat dari user 1 datang: profil tanpa flag berganti ke Ada meskipun user 2 yang dipilih, sedangkan profil dengan flag mencatat dropped stale Ada dan tetap menampilkan Grace.

Bug yang sama muncul di kotak pencarian, tab, dan list apa pun yang difilter oleh server. Contoh loading di atas tidak memakai flag agar tetap ringkas, tetapi setiap fetch di dalam effect sebaiknya memakainya, atau memakai abort di bawah ini.

Membatalkan dengan AbortController

Flag ignore membuang respons yang usang, tetapi request-nya tetap berjalan sampai selesai. Dengan fetch sungguhan, kamu bisa membatalkannya. Buat AbortController di effect, kirim signal-nya ke fetch, lalu panggil abort() di cleanup:

useEffect(() => {
    const controller = new AbortController();

    async function load() {
        try {
            const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            setUser(await res.json());
        } catch (err) {
            if (err.name === 'AbortError') return; // cancelled on purpose
            setError(err);
        }
    }

    load();
    return () => controller.abort();
}, [id]);

fetch yang dibatalkan akan ditolak dengan AbortError, jadi blok catch melewatinya alih-alih menampilkannya sebagai kegagalan. Abort juga menangani unmount: saat komponen dihapus, cleanup-nya membatalkan request.

Memindahkannya ke custom hook

Loading, error, dan pelindung race condition sama di setiap komponen yang mengambil data. Taruh semuanya di custom hook, dan setiap komponen cukup meminta data dalam satu baris.

fetchPosts didefinisikan di luar komponen, jadi identitasnya tidak pernah berubah dan aman ditaruh di array dependensi. Jika didefinisikan di dalam App, ia akan menjadi fungsi baru di setiap render, effect akan berjalan setelah setiap render, dan karena effect mengatur state, ia tidak akan pernah berhenti mengambil data.

Ganti 500 menjadi 2000 lalu klik tombolnya dua kali berturut-turut: konsol menampilkan dua request, dan hanya postingan untuk topik terakhir yang kamu pilih yang muncul.

Fetch di event handler

Effect dipakai untuk data yang dibutuhkan komponen karena ia tampil di layar: halaman profil memuat profilnya. Saat sebuah request terjadi karena pengguna melakukan sesuatu, seperti menekan Search atau Save, jalankan di event handler. Kamu tahu persis apa pemicunya, dan tidak ada yang berjalan ulang saat state yang tidak berkaitan berubah.

async function handleSubmit(e) {
    e.preventDefault();
    setStatus('saving');
    const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
    setStatus(res.ok ? 'saved' : 'error');
}

Kesalahan umum

Dependensi yang terlewat. Effect yang membaca id tetapi punya [] akan terus mengambil user pertama. Cantumkan setiap nilai yang dipakai request.

Fetch di isi komponen. fetch di luar effect berjalan di setiap render, dan jika ia mengatur state, ia memulai loop.

Mengira fetch akan menolak saat terjadi error. Promise-nya hanya ditolak saat jaringan gagal. Periksa res.ok.

Lupa mengatur ulang loading. Saat id berubah, atur loading kembali ke true, atau data lama tetap di layar tanpa tanda bahwa data baru sedang dimuat.

Kapan memakai library atau framework

Fetch di dalam effect tidak masalah untuk beberapa request. Cara ini tidak melakukan caching: buka profil yang sama dua kali, dan datanya diambil dua kali. Cara ini juga tidak berbagi data antarkomponen, tidak mencoba ulang saat gagal, dan tidak mengambil ulang data saat tab kembali mendapat fokus. Library melakukan semua itu untukmu:

import { useQuery } from '@tanstack/react-query';

function Profile({ id }) {
    const { data, error, isPending } = useQuery({
        queryKey: ['user', id],
        queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
    });

    if (isPending) return <p>Loading...</p>;
    if (error) return <p>{error.message}</p>;
    return <p>{data.name}</p>;
}

TanStack Query dan SWR adalah pilihan umum untuk data di sisi client. Framework melangkah lebih jauh dan memuat data di server sebelum halaman sampai ke browser: Next.js dengan server components, React Router dengan loader. Cara ini menghindari kedipan loading dan rantai request yang terjadi saat data induk harus datang dulu sebelum komponen anak bahkan bisa mulai mengambil data.

React 19: use() dengan Suspense

React 19 menambahkan use, yang membaca promise saat render. Komponen melakukan suspend sampai promise selesai, dan boundary <Suspense> terdekat menampilkan fallback selama menunggu, jadi komponen itu sendiri tidak punya state loading:

import { use, Suspense } from 'react';

function Profile({ userPromise }) {
    const user = use(userPromise);
    return <p>{user.name}</p>;
}

<Suspense fallback={<p>Loading...</p>}>
    <Profile userPromise={userPromise} />
</Suspense>

Promise-nya harus dibuat di luar komponen (oleh framework, cache, atau induknya), bukan di dalam render yang membacanya, atau setiap render akan memulai request baru. Halaman hook use membahas ini dengan contoh yang bisa dijalankan.

Pertanyaan yang sering diajukan

Bagaimana cara mengambil data saat komponen React dimuat?

Mulai request di useEffect dengan nilai yang menjadi dependensinya di array dependensi ([] jika tidak ada), lalu simpan hasilnya di state dengan useState. Render pesan loading sampai datanya datang.

Kenapa callback useEffect tidak boleh async?

Fungsi async selalu mengembalikan promise, sedangkan React mengharapkan effect tidak mengembalikan apa pun atau mengembalikan fungsi cleanup. Tulis fungsi async di dalam effect lalu langsung panggil.

Apa itu race condition dalam fetch data di React?

Saat input berubah dengan cepat, ada dua request yang sedang berjalan, dan yang lebih lama bisa menjawab paling akhir sehingga menimpa data yang lebih baru. Atur flag ignore di cleanup effect dan lewati setState saat flag itu aktif, atau batalkan request dengan AbortController.

Sebaiknya saya memakai useEffect atau library untuk fetch data?

useEffect cukup untuk aplikasi kecil dan layak dipahami. Untuk caching, deduplikasi, retry, dan refetch, library seperti TanStack Query atau SWR, atau pemuatan data bawaan framework seperti Next.js, membebaskanmu dari menulis logika itu sendiri.

Bagaimana cara menampilkan spinner loading selama data dimuat?

Simpan boolean loading (atau string status) di state, atur sebelum request dimulai, lalu hapus saat request selesai, baik di jalur berhasil maupun jalur error. Render spinner selama nilainya true.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai