kodingup.camp

Beranda/Various Library Mastery

Sinkronisasi Data dari Server dengan TanStack Query

Modul 2 dari 134 menit bacaRemote Data Synchronization

Isi modul

    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:

    Caching, staleTime, dan gcTime

    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

    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

    Latihan

    1. Mudah, Buat komponen UserProfile yang mengambil https://jsonplaceholder.typicode.com/users/1 dengan useQuery dan menampilkan nama serta email.
    2. Sedang, Buat komponen PostDetail yang menerima prop postId, mengambil /posts/{postId}, dan menampilkan judul. Pastikan key-nya benar. Tambahkan dua tombol "Post 1" dan "Post 2" di parent.
    3. Menantang, Atur staleTime khusus 5 menit hanya untuk query users (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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.