Kamu pasti pernah menulis komponen React yang mengambil data dari API. Polanya hampir selalu identik: satu state untuk loading, satu state untuk data, satu lagi untuk error, lalu useEffect yang memanggil fetch, plus cleanup biar tidak setState setelah komponen unmount. Lima baris logika berubah jadi tiga puluh baris boilerplate. Tambah kebutuhan refetch, caching, atau retry, dan kodenya makin menggembung. TanStack Query menyelesaikan ini. Kalau kamu sudah paham dasar React seperti komponen, state, dan props, artikel ini langsung bisa kamu praktikkan. Kalau belum, mulai dulu dari Belajar React untuk Pemula.
Kenapa useEffect Manual Menyakitkan
Masalahnya bukan cuma boilerplate. Dengan useEffect manual, setiap komponen yang butuh data yang sama akan memanggil API sendiri-sendiri. Buka halaman dengan tiga komponen yang mengambil daftar user, dan kamu dapat tiga request identik ke server. Tidak ada cache, tidak ada deduplikasi. Lalu kalau tab browser kembali aktif setelah lama ditinggal, data tetap basi sampai user refresh manual. Belum lagi race condition: request lama yang selesai belakangan bisa menimpa hasil request baru. TanStack Query menangani semua itu dengan satu API yang konsisten.
Setup: QueryClientProvider
Mulai dari instalasi. TanStack Query tidak terikat backend apa pun, jadi ia bekerja dengan REST, GraphQL, atau endpoint apa saja yang bisa kamu akses via fetch. Kalau backend kamu ditulis dengan FastAPI, query-nya tetap sama persis.
npm install @tanstack/react-query
Langkah kedua: bungkus aplikasi dengan QueryClientProvider. Satu QueryClient memegang seluruh cache dan konfigurasi. Taruh provider di titik paling atas, biasanya di main.jsx atau App.jsx.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
);
}
useQuery: Ambil Data Tanpa Loading State Manual
Ini inti dari seluruh library. useQuery menerima dua hal penting: queryKey dan queryFn. Kamu tidak perlu menyentuh useState atau useEffect sama sekali. Perhatikan objek fetch di bawah ini — ia mengembalikan promise, persis seperti yang kamu pelajari di Async/Await di JavaScript.
import { useQuery } from "@tanstack/react-query";
const fetchTodos = () =>
fetch("https://jsonplaceholder.typicode.com/todos?_limit=10").then((res) => {
if (!res.ok) throw new Error("Gagal mengambil data");
return res.json();
});
function TodoList() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
if (isPending) return <p>Memuat todo...</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.title}
</li>
))}
</ul>
);
}
isPending menggantikan state loading, isError plus error menggantikan state error, dan data sudah bertipe hasil parsing JSON. Tidak ada lagi undefined di render pertama yang bikin crash. Request juga otomatis di-retry tiga kali kalau gagal, lalu berhenti.
queryKey adalah identitas cache. Selama key-nya sama, semua komponen memakai data cache yang sama dan hanya satu request yang dikirim. Ubah key, dan itu dianggap data baru. Jangan pernah menaruh nilai acak atau timestamp di dalamnya, kecuali kamu memang ingin cache selalu dianggap basi.
Mutasi Data dengan useMutation
Membaca data itu setengah cerita. Untuk membuat atau mengubah data, pakai useMutation. Kuncinya ada di onSuccess: begitu mutasi berhasil, kita panggil invalidateQueries supaya TanStack Query menandai cache ["todos"] basi dan otomatis me-refetch daftar terbaru. Satu baris itu menggantikan sekumpulan state manual yang biasanya kamu tulis untuk memperbarui list.
import { useMutation, useQueryClient } from "@tanstack/react-query";
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title) =>
fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, completed: false }),
}).then((res) => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
function handleSubmit(e) {
e.preventDefault();
const title = e.target.title.value.trim();
if (!title) return;
mutation.mutate(title);
e.target.reset();
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Todo baru..." />
<button disabled={mutation.isPending}>
{mutation.isPending ? "Menyimpan..." : "Tambah"}
</button>
</form>
);
}
JSONPlaceholder adalah API tiruan. Ia menerima POST dan mengembalikan respons 201, tapi tidak benar-benar menyimpan datanya. Buat latihan ini tidak masalah, karena kita ingin melihat alur cache dan refetch, bukan persistensi. Kalau mau data benar-benar tersimpan, arahkan fetch ke backend milikmu sendiri — misalnya API dari FastAPI.
Mengatur Cache: staleTime dan gcTime
Default TanStack Query sudah bagus untuk kebanyakan kasus, tapi dua opsi ini sering bikin orang salah paham. Keduanya berkaitan dengan cache, dengan peran yang berbeda.
const { data } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5, // 5 menit: data dianggap segar, tidak refetch
gcTime: 1000 * 60 * 30, // 30 menit: cache disimpan setelah tidak terpakai
});
staleTime menentukan berapa lama data dianggap segar. Selama masih segar, TanStack Query tidak akan mengirim request baru walau komponen di-remount. gcTime menentukan berapa lama data disimpan di memori setelah tidak ada lagi komponen yang memakainya. Pendek staleTime dan panjang gcTime adalah kombinasi umum untuk data yang jarang berubah.
Jangan tertukar: staleTime mengatur seberapa sering refetch terjadi, bukan berapa lama data disimpan. Data yang sudah basi tetap langsung dikembalikan ke komponen (dari cache) sambil request baru berjalan di latar belakang. Kalau kamu memperpanjang staleTime terlalu jauh, user bisa melihat data lama tanpa sadar.
Refetch Otomatis dan Devtools
TanStack Query juga me-refetch data basi secara otomatis saat jendela browser kembali fokus atau saat koneksi kembali online. Perilaku ini bisa dimatikan lewat opsi refetchOnWindowFocus dan refetchOnReconnect, tapi untuk kebanyakan aplikasi, membiarkannya aktif justru menyelamatkanmu dari data basi. Untuk melihat isi cache dan memicu refetch manual saat development, pasang devtools-nya.
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// taruh di dalam QueryClientProvider, sebagai anak terakhir
<ReactQueryDevtools initialIsOpen={false} />
Devtools menunjukkan setiap query yang aktif, statusnya (fresh atau stale), dan tombol untuk refetch, invalidate, atau menghapus cache. Saat bug data muncul, buka devtools dulu sebelum menambah console.log. Langkah lanjut yang bisa kamu coba: optimistik update lewat onMutate, infinite scroll dengan useInfiniteQuery, atau prefetch data sebelum user pindah halaman dengan queryClient.prefetchQuery.