Kenapa ini penting
Hampir semua aplikasi nyata mengambil data dari server. Di modul sebelumnya kamu melihat betapa repotnya cara manual. TanStack Query (dulu bernama React Query) menyelesaikan masalah itu: ia mengurus loading, error, cache, refresh di latar belakang, deduplikasi request, dan retry, cukup dengan satu hook.
Analogi: TanStack Query itu seperti asisten yang memegang "foto papan pengumuman" tadi. Ia menyimpan fotonya (cache), tahu kapan foto itu sudah tua (stale), dan memotret ulang diam-diam saat kamu kembali ke ruangan (refetch on window focus). Kalau dua temanmu minta foto yang sama di saat bersamaan, ia hanya memotret sekali (deduplikasi).
Versi yang dipakai: @tanstack/react-query 5.104.5 dan @tanstack/react-query-devtools 5.104.5.
Konsep inti
Instalasi
npm install @tanstack/react-query
npm install -D @tanstack/react-query-devtools
QueryClient dan QueryClientProvider
QueryClient adalah "gudang" tempat semua cache disimpan. Kamu membuatnya sekali, lalu membungkus aplikasi dengan QueryClientProvider supaya semua komponen di dalamnya bisa memakai gudang yang sama. Polanya mirip Context yang sudah kamu kenal.
useQuery dengan object signature (v5)
Di TanStack Query v5, useQuery hanya menerima satu object. Tutorial lama yang menulis useQuery(['todos'], fetchTodos) sudah tidak berlaku.
const { data, isPending, isError, error, isFetching } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
Beberapa nilai yang dikembalikan:
| Properti | Arti |
|---|---|
data |
Hasil dari queryFn (undefined kalau belum ada) |
isPending |
Belum ada data sama sekali (pertama kali memuat) |
isError / error |
Request gagal, beserta objek error-nya |
isSuccess |
Data sudah ada |
isFetching |
Sedang mengambil data, termasuk refresh di latar belakang |
refetch |
Fungsi untuk mengambil ulang secara manual |
Perbedaan isPending dan isFetching penting: isPending true hanya saat belum ada data. Saat refresh di belakang layar, isPending false (data lama masih ditampilkan) tetapi isFetching true. Di v4 namanya isLoading; di v5 status awal disebut pending.
queryKey: alamat cache
queryKey adalah array yang menjadi "label laci" di gudang cache. Aturannya:
- Data berbeda = key berbeda.
["todos"]untuk semua tugas,["todos", 5]untuk tugas dengan id 5,["todos", { status: "selesai" }]untuk hasil filter. - Semua variabel yang dipakai di
queryFnharus masuk kequeryKey. KalauqueryFnmemakaiuserId, maka key-nya["todos", userId]. SaatuserIdberubah, TanStack Query otomatis mengambil data baru.
Caching, staleTime, dan gcTime
- staleTime (default
0): berapa lama data dianggap "segar". Selama segar, TanStack Query tidak akan mengambil ulang. Default 0 artinya data langsung dianggap basi, jadi akan di-refetch saat komponen mount ulang, saat jendela browser kembali fokus, atau saat koneksi kembali. - gcTime (default 5 menit): berapa lama data yang tidak dipakai komponen mana pun tetap disimpan sebelum dibuang. Di v4 namanya
cacheTime.
Contoh: dengan staleTime: 60_000, kalau kamu pindah halaman lalu kembali dalam 1 menit, data langsung muncul dari cache tanpa request baru.
Fitur otomatis lain
- Retry: request gagal dicoba ulang 3 kali (dengan jeda makin lama) sebelum
isErrormenjadi true. - Refetch on window focus: saat kamu kembali ke tab, data basi diambil ulang.
- Deduplikasi: dua komponen dengan
queryKeysama hanya memicu satu request.
DevTools
ReactQueryDevtools menampilkan panel kecil di pojok layar yang menunjukkan isi cache: key apa saja yang ada, statusnya (fresh, stale, fetching, inactive), dan datanya. Panel ini otomatis tidak ikut ke build produksi.
Contoh kode
// src/main.jsx, menyiapkan QueryClient
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import App from "./App.jsx";
import "./index.css";
// Dibuat SEKALI di luar komponen, supaya cache tidak hilang saat re-render
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // data dianggap segar 30 detik
retry: 2, // coba ulang maksimal 2 kali
},
},
});
createRoot(document.getElementById("root")).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>
);
// src/TodoList.jsx, mengambil data dengan useQuery
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
async function fetchTodos(userId) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos?userId=${userId}&_limit=5`
);
// fetch TIDAK melempar error untuk 404/500, jadi kita lempar sendiri
if (!res.ok) throw new Error(`Gagal memuat (HTTP ${res.status})`);
return res.json();
}
export default function TodoList() {
const [userId, setUserId] = useState(1);
const { data, isPending, isError, error, isFetching, refetch } = useQuery({
queryKey: ["todos", userId], // userId masuk key karena dipakai di queryFn
queryFn: () => fetchTodos(userId),
});
return (
<div className="mx-auto max-w-md space-y-4 p-6">
<div className="flex items-center gap-2">
<select
className="rounded border px-2 py-1"
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
>
<option value={1}>User 1</option>
<option value={2}>User 2</option>
</select>
<button className="rounded bg-slate-800 px-3 py-1 text-white" onClick={() => refetch()}>
Muat ulang
</button>
{isFetching && <span className="text-sm text-slate-500">menyegarkan...</span>}
</div>
{isPending && <p>Memuat tugas...</p>}
{isError && <p className="text-red-600">{error.message}</p>}
{data && (
<ul className="list-disc pl-5">
{data.map((t) => (
<li key={t.id} className={t.completed ? "line-through" : ""}>
{t.title}
</li>
))}
</ul>
)}
</div>
);
}
// → Pilih "User 2": muncul tugas user 2 (request baru, key ["todos", 2]).
// → Kembali ke "User 1" dalam 30 detik: langsung tampil dari cache, tanpa "Memuat tugas...".
Coba buka panel DevTools (ikon bunga di pojok kanan bawah). Kamu akan melihat dua entri: ["todos",1] dan ["todos",2], masing-masing dengan status fresh atau stale.
Kesalahan umum
- Membuat
new QueryClient()di dalam komponen. Setiap render akan membuat gudang baru dan cache hilang. Buat di luar komponen (atau diuseState(() => new QueryClient())). - Lupa memasukkan variabel ke
queryKey. KalauqueryFnpakaiuserIdtapi key-nya cuma["todos"], ganti user tidak akan memicu request baru dan data user lama tetap tampil. - Mengandalkan
fetchmelempar error untuk 404/500.fetchhanya gagal saat jaringan putus. Cekres.okdanthrowsendiri, kalau tidak TanStack Query mengira request sukses. - Memakai sintaks v4.
useQuery(['todos'], fn),isLoadingsebagai status awal,cacheTime, dankeepPreviousData: truesudah diganti di v5 (object signature,isPending,gcTime,placeholderData: keepPreviousData). - Menyalin
datakeuseState. Tidak perlu; pakaidatalangsung. Menyalin membuat dua sumber kebenaran yang bisa tidak sinkron.
Latihan
- Mudah, Buat komponen
UserProfileyang mengambilhttps://jsonplaceholder.typicode.com/users/1denganuseQuerydan menampilkan nama serta email. - Sedang, Buat komponen
PostDetailyang menerima proppostId, mengambil/posts/{postId}, dan menampilkan judul. Pastikan key-nya benar. Tambahkan dua tombol "Post 1" dan "Post 2" di parent. - Menantang, Atur
staleTimekhusus 5 menit hanya untuk queryusers(bukan global), lalu buktikan lewat DevTools bahwa data tetap fresh setelah kamu pindah tab lalu kembali.
Lihat solusi
// 1. UserProfile.jsx
import { useQuery } from "@tanstack/react-query";
export function UserProfile() {
const { data, isPending, isError, error } = useQuery({
queryKey: ["users", 1],
queryFn: async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Gagal memuat user");
return res.json();
},
});
if (isPending) return <p>Memuat...</p>;
if (isError) return <p>{error.message}</p>;
return (
<p>
{data.name} - {data.email}
</p>
);
}
// → Leanne Graham, Sincere@april.biz
// 2. PostDetail.jsx + parent
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
function PostDetail({ postId }) {
const { data, isPending } = useQuery({
queryKey: ["posts", postId], // postId wajib masuk key
queryFn: async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
if (!res.ok) throw new Error("Gagal memuat post");
return res.json();
},
});
if (isPending) return <p>Memuat...</p>;
return <h2>{data.title}</h2>;
}
export function PostPage() {
const [id, setId] = useState(1);
return (
<div>
<button onClick={() => setId(1)}>Post 1</button>
<button onClick={() => setId(2)}>Post 2</button>
<PostDetail postId={id} />
</div>
);
}
// 3. staleTime per query (menimpa default global)
const { data } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5 menit
});
// Di DevTools, entri ["users"] tetap berlabel "fresh" selama 5 menit.
// Pindah tab lalu kembali: tidak ada request baru di tab Network.
Ringkasan
- Bungkus aplikasi dengan
QueryClientProviderdan satuQueryClientyang dibuat di luar komponen. - v5 memakai object signature:
useQuery({ queryKey, queryFn }); status awal =isPending. queryKeyadalah alamat cache; semua variabel yang dipakaiqueryFnwajib masuk key.staleTime= lama data dianggap segar (default 0);gcTime= lama cache tak terpakai disimpan (default 5 menit).- TanStack Query otomatis melakukan retry, refetch saat fokus, dan deduplikasi; DevTools membantu melihat isi cache.