kodingup.camp

Beranda/Various Library Mastery

Konfigurasi Request Global dan Integrasi

Modul 9 dari 134 menit bacaGlobal Request Configuration

Isi modul

    Kenapa ini penting

    Di dua modul sebelumnya kita membuat instance axios dan interceptor. Sekarang pertanyaannya: di mana semua itu diletakkan supaya rapi? Dan bagaimana kalau alamat API saat development (http://localhost:3000) berbeda dengan saat produksi (https://api.tokokamu.com)? Mengganti URL manual setiap mau deploy adalah resep bencana.

    Modul ini menyatukan semuanya: satu API client terpusat, environment variable Vite untuk konfigurasi per lingkungan, dan pola menghubungkan axios dengan TanStack Query sehingga komponen tetap bersih.

    Konsep inti

    Struktur folder yang disarankan

    src/
    ├── lib/
    │   ├── api.js            ← instance axios + interceptor (satu-satunya tempat)
    │   └── queryClient.js    ← konfigurasi QueryClient
    ├── features/
    │   └── todos/
    │       ├── todoApi.js    ← fungsi request khusus todos
    │       └── useTodos.js   ← custom hook TanStack Query
    └── components/
    

    Analogi: api.js itu seperti dapur pusat restoran waralaba. Semua cabang (fitur) memesan bahan dari dapur pusat dengan standar yang sama. Kalau resep bumbu diubah (misalnya cara menempel token), cukup ubah di dapur pusat.

    Ada tiga lapis tanggung jawab:

    1. api.js, cara berbicara dengan server (baseURL, timeout, token, error).
    2. todoApi.js, apa yang diminta (endpoint, params), mengembalikan data bersih.
    3. useTodos.js, kapan dan bagaimana data di-cache (queryKey, staleTime, mutation).

    Komponen hanya memanggil hook di lapis ketiga.

    Environment variable di Vite

    Vite membaca file .env di root proyek. Hanya variabel berawalan VITE_ yang diekspos ke kode browser, lewat import.meta.env:

    # .env (dipakai di semua mode)
    VITE_APP_NAME=Dashboard Tugas
    
    # .env.development (dipakai saat npm run dev)
    VITE_API_URL=https://jsonplaceholder.typicode.com
    
    # .env.production (dipakai saat npm run build)
    VITE_API_URL=https://jsonplaceholder.typicode.com
    
    import.meta.env.VITE_API_URL; // → "https://jsonplaceholder.typicode.com"
    import.meta.env.MODE;         // → "development" atau "production"
    import.meta.env.DEV;          // → true saat npm run dev
    import.meta.env.PROD;         // → true di hasil build
    

    Hal yang perlu diingat:

    Integrasi axios dengan TanStack Query

    TanStack Query tidak peduli kamu memakai fetch atau axios: queryFn cukup mengembalikan Promise yang berisi data, atau melempar error kalau gagal. axios cocok sekali karena sudah melempar error untuk status 4xx/5xx.

    Ada juga fitur pembatalan: TanStack Query memberikan signal (AbortSignal) ke queryFn. Kalau komponen hilang sebelum request selesai atau query dibatalkan, request bisa ikut dibatalkan. axios mendukung opsi signal:

    queryFn: ({ signal }) => api.get("/todos", { signal }).then((r) => r.data),
    

    Query key factory

    Supaya key tidak salah ketik di banyak tempat, kumpulkan dalam satu object:

    export const todoKeys = {
      all: ["todos"],
      list: (filter) => ["todos", "list", filter],
      detail: (id) => ["todos", "detail", id],
    };
    

    Lalu invalidateQueries({ queryKey: todoKeys.all }) otomatis menyentuh semua list dan detail.

    Alur lengkap sebuah request

    Supaya gambaran besarnya jelas, ikuti perjalanan satu klik "centang tugas":

    1. Komponen memanggil update.mutate({ id, completed }) dari hook useUpdateTodo.
    2. Hook menjalankan patchTodo di todoApi.js, yang memanggil api.patch(...).
    3. Request interceptor di api.js menempelkan token, lalu request berangkat ke VITE_API_URL.
    4. Server membalas. Response interceptor menerjemahkan error (kalau ada) menjadi pesan ramah.
    5. Kembali di hook, onSuccess memperbarui cache; semua komponen yang memakai key terkait otomatis menampilkan data baru.

    Setiap lapis punya satu tugas. Kalau suatu hari API pindah alamat, kamu mengubah satu baris di .env. Kalau cara autentikasi berubah, kamu mengubah satu interceptor. Kalau desain daftar berubah, kamu hanya menyentuh komponen. Inilah yang dimaksud kode yang mudah dirawat.

    Contoh nyata: banyak tim memakai tiga lingkungan, development (laptop), staging (server uji), dan production. Dengan pola ini, aplikasi yang sama cukup di-build dengan .env berbeda, tanpa satu baris kode pun berubah.

    Contoh kode

    # .env.development
    VITE_API_URL=https://jsonplaceholder.typicode.com
    VITE_API_TIMEOUT=10000
    
    // src/lib/api.js
    import axios from "axios";
    
    const baseURL = import.meta.env.VITE_API_URL;
    if (!baseURL) {
      // gagal cepat dengan pesan jelas, daripada request ke "undefined/todos"
      throw new Error("VITE_API_URL belum diatur di file .env");
    }
    
    export const api = axios.create({
      baseURL,
      timeout: Number(import.meta.env.VITE_API_TIMEOUT ?? 10000),
    });
    
    api.interceptors.request.use((config) => {
      const token = localStorage.getItem("token");
      if (token) config.headers.Authorization = `Bearer ${token}`;
      if (import.meta.env.DEV) console.debug(`[api] ${config.method?.toUpperCase()} ${config.url}`);
      return config;
    });
    
    api.interceptors.response.use(
      (res) => res,
      (error) => {
        if (axios.isAxiosError(error)) {
          const status = error.response?.status;
          if (status === 404) error.message = "Data tidak ditemukan.";
          else if (status >= 500) error.message = "Server sedang bermasalah.";
          else if (!error.response) error.message = "Tidak bisa terhubung ke server.";
        }
        return Promise.reject(error);
      }
    );
    
    // src/lib/queryClient.js
    import { QueryClient } from "@tanstack/react-query";
    
    export const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 60_000,
          retry: (jumlahGagal, error) => {
            const status = error?.response?.status;
            if (status && status < 500) return false; // 4xx tidak perlu dicoba ulang
            return jumlahGagal < 2;
          },
          refetchOnWindowFocus: import.meta.env.PROD, // saat dev tidak refetch tiap pindah tab
        },
      },
    });
    
    // src/features/todos/todoApi.js
    import { api } from "../../lib/api";
    
    export const todoKeys = {
      all: ["todos"],
      list: (params) => ["todos", "list", params],
      detail: (id) => ["todos", "detail", id],
    };
    
    export async function fetchTodos(params, signal) {
      const { data } = await api.get("/todos", { params, signal });
      return data;
    }
    
    export async function fetchTodo(id, signal) {
      const { data } = await api.get(`/todos/${id}`, { signal });
      return data;
    }
    
    export async function patchTodo({ id, ...perubahan }) {
      const { data } = await api.patch(`/todos/${id}`, perubahan);
      return data;
    }
    
    // src/features/todos/useTodos.js
    import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
    import { fetchTodo, fetchTodos, patchTodo, todoKeys } from "./todoApi";
    
    export function useTodos(params = { userId: 1, _limit: 10 }) {
      return useQuery({
        queryKey: todoKeys.list(params),
        queryFn: ({ signal }) => fetchTodos(params, signal),
      });
    }
    
    export function useTodo(id) {
      return useQuery({
        queryKey: todoKeys.detail(id),
        queryFn: ({ signal }) => fetchTodo(id, signal),
        enabled: Boolean(id), // jangan jalan kalau id belum ada
      });
    }
    
    export function useUpdateTodo() {
      const queryClient = useQueryClient();
      return useMutation({
        mutationFn: patchTodo,
        onSuccess: (todoBaru) => {
          // perbarui semua list yang memuat todo ini (jsonplaceholder tidak menyimpan, jadi tidak invalidate)
          queryClient.setQueriesData({ queryKey: ["todos", "list"] }, (lama) =>
            Array.isArray(lama) ? lama.map((t) => (t.id === todoBaru.id ? todoBaru : t)) : lama
          );
          queryClient.setQueryData(todoKeys.detail(todoBaru.id), todoBaru);
        },
      });
    }
    
    // src/features/todos/TodoList.jsx, komponen jadi sangat bersih
    import { useTodos, useUpdateTodo } from "./useTodos";
    
    export function TodoList() {
      const { data: todos = [], isPending, isError, error } = useTodos();
      const update = useUpdateTodo();
    
      if (isPending) return <p>Memuat...</p>;
      if (isError) return <p className="text-red-600">{error.message}</p>;
    
      return (
        <ul className="space-y-1">
          {todos.map((t) => (
            <li key={t.id}>
              <label className="flex gap-2">
                <input
                  type="checkbox"
                  checked={t.completed}
                  onChange={() => update.mutate({ id: t.id, completed: !t.completed })}
                />
                {t.title}
              </label>
            </li>
          ))}
        </ul>
      );
    }
    // → 10 tugas milik user 1. Mencentang memanggil PATCH lalu memperbarui cache list dan detail.
    

    Kesalahan umum

    Latihan

    1. Mudah, Tambahkan VITE_APP_NAME di .env dan tampilkan di judul halaman (komponen header).
    2. Sedang, Buat userApi.js dan useUsers.js mengikuti pola todos: userKeys, fetchUsers, dan hook useUsers() yang mengambil /users.
    3. Menantang, Buat hook useTodo(id) dipakai di komponen detail, dan atur initialData dari cache list supaya detail tampil instan saat diklik dari daftar (petunjuk: queryClient.getQueriesData).
    Lihat solusi
    // 1.
    // .env → VITE_APP_NAME=Dashboard Tugas
    export function Header() {
      return <h1 className="text-xl font-bold">{import.meta.env.VITE_APP_NAME}</h1>;
    }
    // → Dashboard Tugas
    
    // 2. src/features/users/userApi.js
    import { api } from "../../lib/api";
    export const userKeys = { all: ["users"], detail: (id) => ["users", id] };
    export async function fetchUsers(signal) {
      const { data } = await api.get("/users", { signal });
      return data;
    }
    
    // src/features/users/useUsers.js
    import { useQuery } from "@tanstack/react-query";
    import { fetchUsers, userKeys } from "./userApi";
    export function useUsers() {
      return useQuery({
        queryKey: userKeys.all,
        queryFn: ({ signal }) => fetchUsers(signal),
        staleTime: 10 * 60 * 1000, // data user jarang berubah
      });
    }
    
    // 3. initialData dari cache list
    import { useQuery, useQueryClient } from "@tanstack/react-query";
    import { fetchTodo, todoKeys } from "./todoApi";
    
    export function useTodo(id) {
      const queryClient = useQueryClient();
      return useQuery({
        queryKey: todoKeys.detail(id),
        queryFn: ({ signal }) => fetchTodo(id, signal),
        enabled: Boolean(id),
        initialData: () => {
          // cari di semua list yang sudah ada di cache
          for (const [, list] of queryClient.getQueriesData({ queryKey: ["todos", "list"] })) {
            const ketemu = Array.isArray(list) ? list.find((t) => t.id === id) : undefined;
            if (ketemu) return ketemu;
          }
          return undefined; // tidak ada → fetch seperti biasa
        },
      });
    }
    // → Klik tugas dari daftar: detail langsung tampil tanpa "Memuat...".
    

    Ringkasan

    Kuis pemahaman

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