Kenapa ini penting
Aplikasi nyata hampir selalu menampilkan data dari server: daftar produk, profil user, riwayat transaksi. Data itu tidak ada saat komponen pertama kali dirender. Ia harus diambil lewat jaringan, yang butuh waktu dan bisa gagal. Di modul ini kamu belajar cara paling dasar mengambil data di React: fetch di dalam useEffect. Memahami cara manual ini penting, karena library data fetching seperti TanStack Query, SWR, atau fitur data di framework seperti Next.js pada dasarnya menyelesaikan masalah-masalah yang akan kamu temui di sini.
Konsep inti
Kenapa di useEffect?
Render di React harus murni: hanya menghitung JSX dari props dan state, tanpa efek samping. Mengambil data dari jaringan adalah efek samping, karena ia berinteraksi dengan dunia luar. Tempatnya di useEffect, yang dijalankan setelah React menampilkan hasil render ke layar.
Kalau kamu memanggil fetch langsung di badan komponen lalu setState, setiap setState memicu render, render memanggil fetch lagi, dan seterusnya: loop tak berujung.
Fungsi async di dalam effect
Callback useEffect tidak boleh berupa fungsi async, karena fungsi async selalu mengembalikan Promise, sedangkan React mengharapkan callback mengembalikan fungsi cleanup atau tidak mengembalikan apa-apa. Caranya: definisikan fungsi async di dalam effect, lalu panggil.
Race condition dan cleanup
Bayangkan user memilih user ID 1, lalu cepat-cepat pindah ke ID 2. Dua request berjalan. Kalau respons ID 1 datang lebih lambat, ia akan menimpa data ID 2 dan layar menampilkan data yang salah. Ini disebut race condition.
Solusi yang dicontohkan dokumentasi React: variabel ignore di dalam effect. Fungsi cleanup mengubahnya menjadi true ketika effect lama dibatalkan (saat dependensi berubah atau komponen unmount). Respons lama yang datang terlambat akan diabaikan. Alternatifnya, AbortController yang benar-benar membatalkan request di level browser.
Analogi: kamu memesan ojek online, lalu membatalkan dan memesan lagi ke alamat lain. Kalau driver pesanan pertama tetap datang, kamu tidak naik motornya. Variabel ignore adalah tanda "pesanan ini sudah dibatalkan".
Alur data dari API ke layar
- Komponen render pertama: data masih
nullatau[]. - Effect berjalan:
fetch(url). - Tunggu respons, cek
response.ok, lalu ubah ke JSON denganawait res.json(). setData(json)memicu render ulang dengan data.- Tampilkan list dengan
map+key.
Catatan penting: fetch tidak melempar error ketika server membalas 404 atau 500. Ia hanya melempar error untuk masalah jaringan. Jadi kamu wajib mengecek res.ok sendiri.
Di mana fungsi fetch ditaruh?
Supaya komponen tidak penuh URL dan detail HTTP, taruh fungsi pengambil data di file api.js fitur (lihat modul 7). Komponen cukup memanggil getUsers().
Membaca respons API dengan teliti
Sebelum menulis kode tampilan, buka dulu URL API di browser atau lihat tab Network di DevTools. Perhatikan bentuk datanya: apakah berupa array langsung ([...]) atau object yang membungkus array ({ data: [...], total: 100 })? Field apa saja yang tersedia dan apakah ada yang bisa kosong (null)? Banyak bug pemula muncul karena menebak bentuk data, misalnya memanggil data.map padahal array-nya ada di data.items.
Data awal dan render pertama
Nilai awal state menentukan apa yang terjadi di render pertama, sebelum data datang. Kalau kamu memakai useState([]) untuk list, users.map aman dipanggil karena array kosong. Kalau memakai useState(null) untuk satu object, kamu wajib mengecek if (!user) sebelum membaca user.name, karena membaca property dari null akan melempar error. Pilih nilai awal yang paling sesuai dengan bentuk data, lalu tangani kondisi "belum ada data" secara eksplisit.
Keamanan kunci API
Kode React berjalan di browser user, sehingga apa pun yang ditulis di kode frontend, termasuk variabel import.meta.env.VITE_..., bisa dilihat siapa saja. Jangan pernah menaruh kunci API rahasia (misalnya kunci pembayaran) di aplikasi React. Kunci seperti itu harus disimpan di server/backend yang menjadi perantara.
Contoh kode
// src/features/users/api.js
const BASE = 'https://jsonplaceholder.typicode.com';
export async function getUsers({ signal } = {}) {
const res = await fetch(`${BASE}/users`, { signal });
// fetch tidak melempar error untuk 404/500, jadi cek manual
if (!res.ok) throw new Error(`Gagal memuat user (status ${res.status})`);
return res.json();
}
export async function getUser(id) {
const res = await fetch(`${BASE}/users/${id}`);
if (!res.ok) throw new Error(`User ${id} tidak ditemukan`);
return res.json();
}
Menampilkan list dari API:
// src/features/users/components/UserList.jsx
import { useEffect, useState } from 'react';
import { getUsers } from '../api.js';
export function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
let ignore = false; // tanda "hasil ini sudah tidak dibutuhkan"
async function load() {
const data = await getUsers();
if (!ignore) setUsers(data);
}
load();
return () => {
ignore = true; // cleanup: abaikan respons kalau komponen sudah unmount
};
}, []); // [] → ambil sekali saat mount
return (
<ul>
{users.map((u) => (
<li key={u.id}>
{u.name} - {u.email}
</li>
))}
</ul>
);
}
// → Leanne Graham, Sincere@april.biz
// → Ervin Howell, Shanna@melissa.tv
// → ... (10 user)
Mengambil data berdasarkan props, aman dari race condition:
// src/features/users/components/UserDetail.jsx
import { useEffect, useState } from 'react';
import { getUser } from '../api.js';
export function UserDetail({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
getUser(userId).then((data) => {
if (!ignore) setUser(data);
});
return () => {
ignore = true; // userId berubah → respons lama diabaikan
};
}, [userId]); // jalan ulang setiap userId berubah
// data user lama (id beda) dianggap belum siap → tidak ada data basi yang tampil
if (!user || user.id !== userId) return <p>Memuat...</p>;
return (
<article>
<h2>{user.name}</h2>
<p>Kota: {user.address.city}</p>
</article>
);
}
// Pemakaian dengan pemilih ID
export function UserPicker() {
const [id, setId] = useState(1);
return (
<>
<select value={id} onChange={(e) => setId(Number(e.target.value))}>
{[1, 2, 3].map((n) => (
<option key={n} value={n}>User {n}</option>
))}
</select>
<UserDetail userId={id} />
</>
);
}
// pilih User 2 → "Ervin Howell", Kota: Wisokyburgh
Versi AbortController (benar-benar membatalkan request):
useEffect(() => {
const controller = new AbortController();
getUsers({ signal: controller.signal })
.then(setUsers)
.catch((err) => {
// AbortError terjadi wajar saat cleanup, bukan error sungguhan
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, []);
Kesalahan umum
- Menulis
useEffect(async () => {...}). Callback effect jadi mengembalikan Promise. Definisikan fungsi async di dalam effect lalu panggil. - Lupa dependency array. Tanpa
[], effect berjalan setiap render;setUsersmemicu render; effect jalan lagi: request tanpa henti. - Tidak mengecek
res.ok. Respons 404 tetap dianggap sukses olehfetch, lalures.json()bisa gagal atau berisi pesan error yang kamu tampilkan sebagai data. - Mengabaikan race condition. Tanpa
ignoreatauAbortController, data lama bisa menimpa data baru saat props berubah cepat. - Kaget melihat request terkirim dua kali di development. Itu karena StrictMode sengaja menjalankan effect dua kali (dibahas di modul 13). Dengan cleanup yang benar, hasil tampilan tetap benar.
Latihan
- Mudah, Ambil daftar post dari
https://jsonplaceholder.typicode.com/posts?_limit=5dan tampilkan judulnya. - Sedang, Buat komponen
PostDetailyang menerimapostIddan mengambil/posts/{id}, aman dari race condition dengan polaignore. - Menantang, Buat halaman dengan input angka
userIddan tampilkan semua todo user itu dari/todos?userId={id}memakaiAbortController, dengan jumlah todo yang selesai di bagian atas (hitung sebagai data turunan).
Lihat solusi
// 1.
import { useEffect, useState } from 'react';
export function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
let ignore = false;
async function load() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
const data = await res.json();
if (!ignore) setPosts(data);
}
load();
return () => { ignore = true; };
}, []);
return <ol>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ol>;
}
// 2.
import { useEffect, useState } from 'react';
export function PostDetail({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((res) => res.json())
.then((data) => { if (!ignore) setPost(data); });
return () => { ignore = true; };
}, [postId]);
if (!post || post.id !== postId) return <p>Memuat...</p>;
return <h2>{post.title}</h2>;
}
// 3.
import { useEffect, useState } from 'react';
export function TodoUser() {
const [userId, setUserId] = useState(1);
const [todos, setTodos] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`https://jsonplaceholder.typicode.com/todos?userId=${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setTodos)
.catch((err) => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort();
}, [userId]);
const selesai = todos.filter((t) => t.completed).length; // data turunan
return (
<>
<input type="number" min={1} max={10} value={userId} onChange={(e) => setUserId(Number(e.target.value))} />
<p>{selesai} dari {todos.length} selesai</p>
<ul>{todos.map((t) => <li key={t.id}>{t.completed ? '✅' : '⬜'} {t.title}</li>)}</ul>
</>
);
}
Ringkasan
- Mengambil data adalah efek samping, jadi dilakukan di
useEffect, bukan di badan render. - Callback effect tidak boleh async; buat fungsi async di dalamnya lalu panggil.
fetchtidak melempar error untuk status 404/500; selalu cekres.ok.- Gunakan variabel
ignoreatauAbortControllerdi cleanup untuk mencegah race condition. - Taruh fungsi pemanggil API di file terpisah (
api.js) agar komponen tetap bersih.