Episode ini mengajarkan hook pertama dan terpenting: useQuery. Kalian membuat query dengan queryKey dan queryFn, membaca status data, isLoading, isError, error, dan isFetching, serta mempelajari pola query function yang benar dan aman.

Inilah momen yang kalian tunggu: hook pertama yang benar-benar menggantikan pola useEffect plus fetch manual. Dengan useQuery, kalian mendapat data, status loading, error, dan refetch otomatis dalam satu panggilan hook — tanpa menulis satu pun useState untuk menyimpan hasil fetch.
Episode 4 membedah useQuery dari nol: struktur dasar, status-status yang bisa dibaca, dan pola query function yang benar. Ini adalah hook yang akan kalian pakai paling sering selama berkarir dengan TanStack Query.
useQuery menerima satu objek konfigurasi dengan dua anggota wajib: queryKey dan queryFn.
import { useQuery } from "@tanstack/react-query"
async function fetchTodos() {
const res = await fetch("https://jsonplaceholder.typicode.com/todos")
if (!res.ok) throw new Error("Gagal mengambil todos")
return res.json()
}
function Todos() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
})
if (isLoading) return <p>Memuat...</p>
if (isError) return <p>Terjadi error: {error.message}</p>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}Perhatikan apa yang tidak ada: tidak ada useState, tidak ada useEffect, tidak ada fetch manual di komponen. useQuery mengembalikan sebuah objek status yang langsung bisa dipakai untuk render. queryKey: ["todos"] mengikat data ini ke slot cache todos, dan fetchTodos dipanggil otomatis saat komponen di-mount.
useQuery mengembalikan banyak properti status. Empat yang paling penting untuk pemula:
queryFn yang sudah sukses.queryFn melempar error.queryFn.Ada satu status lagi yang sering tertukar: isFetching, yang true setiap kali fetch berjalan — termasuk refetch di latar belakang. Bedanya: isLoading hanya true saat pertama kali dan belum ada data, sedangkan isFetching true setiap ada aktivitas jaringan.
queryFn harus berupa fungsi yang mengembalikan Promise. Jangan pernah menulis Promise di dalam queryFn yang sudah di-trigger dari luar, dan jangan menaruh queryFn di dalam JSX karena akan dibuat ulang terus menerus. Cukup definisikan sebagai fungsi bernama seperti contoh di atas, atau sebagai arrow function:
const { data } = useQuery({
queryKey: ["todos"],
queryFn: async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/todos")
if (!res.ok) throw new Error("Gagal mengambil todos")
return res.json()
},
})Untuk query yang butuh parameter — misalnya todo berdasarkan ID — query function menerima sebuah context yang berisi queryKey. Kalian bisa memakai parameter yang sudah ada di dalam queryKey:
function TodoDetail({ id }) {
const { data } = useQuery({
queryKey: ["todos", id],
queryFn: ({ queryKey }) => {
const [, todoId] = queryKey
return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`).then(
(res) => res.json()
)
},
})
return <h1>{data?.title}</h1>
}Di sini destructuring const [, todoId] = queryKey mengambil parameter dari array key. queryKey: ["todos", id] membuat setiap id memiliki slot cache sendiri — data todo 1 dan todo 2 tidak akan tertukar.
TanStack Query hanya tahu sebuah query gagal jika queryFn melempar error. Jika kalian mengembalikan res.ok yang salah sebagai data, library menganggap sukses dan tidak akan mencoba ulang. Karena itu pola if (!res.ok) throw new Error(...) di atas adalah kunci — error inilah yang memicu mekanisme retry bawaan.
Warning
Jika queryFn melempar error, TanStack Query akan menghentikan pengambilan data dan mencoba ulang sesuai konfigurasi retry. Pastikan selalu melempar error, bukan mengembalikan respons yang gagal, agar status isError dan error bekerja dengan benar.
Mari lihat penghematan yang didapat. Pola manual dari episode 0 butuh useState, useEffect, dan penanganan loading manual. Dengan useQuery, semua itu digantikan oleh satu hook yang juga memberi cache, retry, dan refetch di latar belakang:
manual: useState + useEffect + fetch + setData + setLoading + setError
useQuery: useQuery( queryKey, queryFn ) → data + isLoading + isError + isFetchingRingkasan manual: useState + useEffect + fetch menunjukkan berapa banyak state yang diurus manual dulu, dan useQuery mengkonsolidasikan semuanya. Belum lagi fitur cache yang sama sekali tidak ada di pola manual.
Episode 4 memberi kalian hook paling fundamental: useQuery. Kalian sekarang bisa membuat query pertama, membaca status data, isLoading, isError, error, dan isFetching, memakai parameter lewat queryKey, serta melempar error dengan benar agar retry bekerja.
Inti yang harus dibawa pulang:
useQuery butuh queryKey dan queryFn yang mengembalikan Promise.isLoading menandakan belum ada data; isFetching menandakan ada aktivitas jaringan.queryKey dan diambil dari context.throw error di queryFn agar status error dan retry bekerja.useState atau useEffect lagi untuk data fetching.queryKey berarti satu slot cache.Di episode 5 selanjutnya kita akan membahas query keys dan caching — struktur array hierarkis untuk query yang kompleks, peran query key dalam deduplication, dan bagaimana structural sharing menjaga performa. Pemahaman ini menjadi kunci untuk invalidasi cache di episode 6.