Hampir semua app React modern butuh ngambil data dari server: profil user, list produk, chat messages, notifikasi. Pattern dasarnya useEffect + fetch — tapi ada banyak edge case yang sering dilupain pemula: loading state, error handling, race condition, cleanup, retry, refetch saat focus, cache. Kamu bisa handle manual semua, atau pakai library spesialis. Lesson ini cover pattern manual dulu (paham fundamental), lalu kasih jalan modern (TanStack Query) yang handle 90% complexity gratis.
💡 Tiga state SELALU ada. Loading (sedang fetch?), error (gagal?), data (sukses?). Pemula sering lupa salah satu — biasanya error handling. UI yang gak handle error = stuck di loading forever pas API down. Pattern wajib: cek loading, cek error, baru render data.
Pattern Manual
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
fetch("/api/users")
.then(res => {
if (!res.ok) throw new Error("Gagal fetch")
return res.json()
})
.then(data => { if (!cancelled) setUsers(data) })
.catch(err => { if (!cancelled) setError(err.message) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [])
if (loading) return <p>Memuat...</p>
if (error) return <p>Error: {error}</p>
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Cancellation dengan AbortController
useEffect(() => {
const controller = new AbortController()
fetch("/api/users", { signal: controller.signal })
.then(res => res.json())
.then(setUsers)
.catch(err => {
if (err.name !== "AbortError") setError(err.message)
})
return () => controller.abort()
}, [])
Custom Hook — Reusable
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then(d => { if (!cancelled) setData(d) })
.catch(e => { if (!cancelled) setError(e.message) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Pemakaian — clean
const { data, loading, error } = useFetch(`/api/users/${id}`)
Modern: TanStack Query (atau SWR)
import { useQuery } from "@tanstack/react-query"
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json())
})
if (isLoading) return <Spinner />
if (error) return <Error />
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Bonus gratis: cache, refetch on focus, retry on error, request deduplication, pagination, infinite scroll, optimistic updates — semua built-in. Project hypersheets-frontend pakai TanStack Query.
🎭 Analogi sehari-hari
Fetching data manual itu kayak bikin sambel dari nol tiap hari: ulek cabe, bawang, garam (loading state), kalo bahan kurang siap-siap (error), terus dipake (data). Boleh aja, tapi capek kalo dilakuin tiap hari. TanStack Query kayak ke warung sambel langganan — bumbu udah disiapin, kalo udah pernah pesen sambel sama, kasih dari stok (cache), kalo gosong sebagian dia retry, kalo lagi gak ada bahan dia kasih notif. Kamu fokus masak, gak buang waktu di urusan persiapan rutin. Manual fetching = belajar fundamental + control penuh. Library = produktivitas + handle edge case yang banyak.
⚠️ Jebakan yang sering ditemui
fetchGAK throw error di status 4xx/5xx — Pemula pikir 404 = catch jalan. SALAH.fetchresolve denganres.ok = false. Wajib cek manual:if (!res.ok) throw new Error().- Race condition — fetch user 1 → user pindah ke user 2 (fetch baru) → user 1 jawab BELAKANGAN, override user 2 data. Pakai
cancelledflag atauAbortController. - Lupa cleanup — component unmount sebelum fetch selesai →
setStatedi unmounted component → warning + memory leak. - Loading initially
true—useState(false)lalu set true di useEffect = flicker UI sebentar. Mulai denganuseState(true). - Error swallowed —
.then(setData).catch(() => {})(silent). Minimum log atau set error state. - Refetch manual ribet — useState
refetchTriggerlalu increment di tombol = anti-pattern. Pakai TanStack Queryrefetch(). - Stale data after navigation — pindah halaman lalu balik, data pakai cache lama. Library handle, manual harus invalidate sendiri.
- Pagination state local — manual pagination = state explosion. TanStack Query
useInfiniteQueryjauh lebih clean.
🎯 Manual atau library — pilih mana?
- Belajar React (1-2 fetch sederhana) → manual
useEffect + fetch✓ paham fundamental- App production dengan banyak endpoint → TanStack Query atau SWR (default modern)
- GraphQL → Apollo Client atau Relay
- Server-side rendering / Next.js → Server Components atau
getServerSideProps- Realtime data → Supabase Realtime, Firebase, atau WebSocket library
- Auth flow + token refresh → library yang handle interceptor (axios + interceptor + TanStack Query)
Aturan praktis: project < 5 endpoint → manual OK. Project production → library. Pattern hampir universal: TanStack Query.
TL;DR: Pattern manual: useEffect + fetch + 3 state (loading, error, data). fetch GAK throw error 4xx/5xx — cek res.ok manual. Wajib cleanup (cancelled flag atau AbortController) cegah race condition. Mulai loading true biar gak flicker. Untuk app production: pakai TanStack Query atau SWR — handle cache, refetch, retry, dedup, optimistic update gratis. Project hypersheets-frontend pakai TanStack Query.