kodingup.camp

Beranda/React Mastery: Styling, Pola Komponen, Data & Optimasi

Integrasi Data Jarak Jauh

Modul 8 dari 134 menit bacaRemote Data Integration

Isi modul

    Kenapa ini penting

    Aplikasi nyata hampir selalu menampilkan data dari server: daftar produk, profil user, riwayat transaksi. Data itu tidak ada saat komponen pertama kali dirender. Ia harus diambil lewat jaringan, yang butuh waktu dan bisa gagal. Di modul ini kamu belajar cara paling dasar mengambil data di React: fetch di dalam useEffect. Memahami cara manual ini penting, karena library data fetching seperti TanStack Query, SWR, atau fitur data di framework seperti Next.js pada dasarnya menyelesaikan masalah-masalah yang akan kamu temui di sini.

    Konsep inti

    Kenapa di useEffect?

    Render di React harus murni: hanya menghitung JSX dari props dan state, tanpa efek samping. Mengambil data dari jaringan adalah efek samping, karena ia berinteraksi dengan dunia luar. Tempatnya di useEffect, yang dijalankan setelah React menampilkan hasil render ke layar.

    Kalau kamu memanggil fetch langsung di badan komponen lalu setState, setiap setState memicu render, render memanggil fetch lagi, dan seterusnya: loop tak berujung.

    Fungsi async di dalam effect

    Callback useEffect tidak boleh berupa fungsi async, karena fungsi async selalu mengembalikan Promise, sedangkan React mengharapkan callback mengembalikan fungsi cleanup atau tidak mengembalikan apa-apa. Caranya: definisikan fungsi async di dalam effect, lalu panggil.

    Race condition dan cleanup

    Bayangkan user memilih user ID 1, lalu cepat-cepat pindah ke ID 2. Dua request berjalan. Kalau respons ID 1 datang lebih lambat, ia akan menimpa data ID 2 dan layar menampilkan data yang salah. Ini disebut race condition.

    Solusi yang dicontohkan dokumentasi React: variabel ignore di dalam effect. Fungsi cleanup mengubahnya menjadi true ketika effect lama dibatalkan (saat dependensi berubah atau komponen unmount). Respons lama yang datang terlambat akan diabaikan. Alternatifnya, AbortController yang benar-benar membatalkan request di level browser.

    Analogi: kamu memesan ojek online, lalu membatalkan dan memesan lagi ke alamat lain. Kalau driver pesanan pertama tetap datang, kamu tidak naik motornya. Variabel ignore adalah tanda "pesanan ini sudah dibatalkan".

    Alur data dari API ke layar

    1. Komponen render pertama: data masih null atau [].
    2. Effect berjalan: fetch(url).
    3. Tunggu respons, cek response.ok, lalu ubah ke JSON dengan await res.json().
    4. setData(json) memicu render ulang dengan data.
    5. Tampilkan list dengan map + key.

    Catatan penting: fetch tidak melempar error ketika server membalas 404 atau 500. Ia hanya melempar error untuk masalah jaringan. Jadi kamu wajib mengecek res.ok sendiri.

    Di mana fungsi fetch ditaruh?

    Supaya komponen tidak penuh URL dan detail HTTP, taruh fungsi pengambil data di file api.js fitur (lihat modul 7). Komponen cukup memanggil getUsers().

    Membaca respons API dengan teliti

    Sebelum menulis kode tampilan, buka dulu URL API di browser atau lihat tab Network di DevTools. Perhatikan bentuk datanya: apakah berupa array langsung ([...]) atau object yang membungkus array ({ data: [...], total: 100 })? Field apa saja yang tersedia dan apakah ada yang bisa kosong (null)? Banyak bug pemula muncul karena menebak bentuk data, misalnya memanggil data.map padahal array-nya ada di data.items.

    Data awal dan render pertama

    Nilai awal state menentukan apa yang terjadi di render pertama, sebelum data datang. Kalau kamu memakai useState([]) untuk list, users.map aman dipanggil karena array kosong. Kalau memakai useState(null) untuk satu object, kamu wajib mengecek if (!user) sebelum membaca user.name, karena membaca property dari null akan melempar error. Pilih nilai awal yang paling sesuai dengan bentuk data, lalu tangani kondisi "belum ada data" secara eksplisit.

    Keamanan kunci API

    Kode React berjalan di browser user, sehingga apa pun yang ditulis di kode frontend, termasuk variabel import.meta.env.VITE_..., bisa dilihat siapa saja. Jangan pernah menaruh kunci API rahasia (misalnya kunci pembayaran) di aplikasi React. Kunci seperti itu harus disimpan di server/backend yang menjadi perantara.

    Contoh kode

    // src/features/users/api.js
    const BASE = 'https://jsonplaceholder.typicode.com';
    
    export async function getUsers({ signal } = {}) {
      const res = await fetch(`${BASE}/users`, { signal });
      // fetch tidak melempar error untuk 404/500, jadi cek manual
      if (!res.ok) throw new Error(`Gagal memuat user (status ${res.status})`);
      return res.json();
    }
    
    export async function getUser(id) {
      const res = await fetch(`${BASE}/users/${id}`);
      if (!res.ok) throw new Error(`User ${id} tidak ditemukan`);
      return res.json();
    }
    

    Menampilkan list dari API:

    // src/features/users/components/UserList.jsx
    import { useEffect, useState } from 'react';
    import { getUsers } from '../api.js';
    
    export function UserList() {
      const [users, setUsers] = useState([]);
    
      useEffect(() => {
        let ignore = false; // tanda "hasil ini sudah tidak dibutuhkan"
    
        async function load() {
          const data = await getUsers();
          if (!ignore) setUsers(data);
        }
        load();
    
        return () => {
          ignore = true; // cleanup: abaikan respons kalau komponen sudah unmount
        };
      }, []); // [] → ambil sekali saat mount
    
      return (
        <ul>
          {users.map((u) => (
            <li key={u.id}>
              {u.name} - {u.email}
            </li>
          ))}
        </ul>
      );
    }
    
    // → Leanne Graham, Sincere@april.biz
    // → Ervin Howell, Shanna@melissa.tv
    // → ... (10 user)
    

    Mengambil data berdasarkan props, aman dari race condition:

    // src/features/users/components/UserDetail.jsx
    import { useEffect, useState } from 'react';
    import { getUser } from '../api.js';
    
    export function UserDetail({ userId }) {
      const [user, setUser] = useState(null);
    
      useEffect(() => {
        let ignore = false;
    
        getUser(userId).then((data) => {
          if (!ignore) setUser(data);
        });
    
        return () => {
          ignore = true; // userId berubah → respons lama diabaikan
        };
      }, [userId]); // jalan ulang setiap userId berubah
    
      // data user lama (id beda) dianggap belum siap → tidak ada data basi yang tampil
      if (!user || user.id !== userId) return <p>Memuat...</p>;
      return (
        <article>
          <h2>{user.name}</h2>
          <p>Kota: {user.address.city}</p>
        </article>
      );
    }
    
    // Pemakaian dengan pemilih ID
    export function UserPicker() {
      const [id, setId] = useState(1);
      return (
        <>
          <select value={id} onChange={(e) => setId(Number(e.target.value))}>
            {[1, 2, 3].map((n) => (
              <option key={n} value={n}>User {n}</option>
            ))}
          </select>
          <UserDetail userId={id} />
        </>
      );
    }
    // pilih User 2 → "Ervin Howell", Kota: Wisokyburgh
    

    Versi AbortController (benar-benar membatalkan request):

    useEffect(() => {
      const controller = new AbortController();
    
      getUsers({ signal: controller.signal })
        .then(setUsers)
        .catch((err) => {
          // AbortError terjadi wajar saat cleanup, bukan error sungguhan
          if (err.name !== 'AbortError') console.error(err);
        });
    
      return () => controller.abort();
    }, []);
    

    Kesalahan umum

    1. Menulis useEffect(async () => {...}). Callback effect jadi mengembalikan Promise. Definisikan fungsi async di dalam effect lalu panggil.
    2. Lupa dependency array. Tanpa [], effect berjalan setiap render; setUsers memicu render; effect jalan lagi: request tanpa henti.
    3. Tidak mengecek res.ok. Respons 404 tetap dianggap sukses oleh fetch, lalu res.json() bisa gagal atau berisi pesan error yang kamu tampilkan sebagai data.
    4. Mengabaikan race condition. Tanpa ignore atau AbortController, data lama bisa menimpa data baru saat props berubah cepat.
    5. Kaget melihat request terkirim dua kali di development. Itu karena StrictMode sengaja menjalankan effect dua kali (dibahas di modul 13). Dengan cleanup yang benar, hasil tampilan tetap benar.

    Latihan

    1. Mudah, Ambil daftar post dari https://jsonplaceholder.typicode.com/posts?_limit=5 dan tampilkan judulnya.
    2. Sedang, Buat komponen PostDetail yang menerima postId dan mengambil /posts/{id}, aman dari race condition dengan pola ignore.
    3. Menantang, Buat halaman dengan input angka userId dan tampilkan semua todo user itu dari /todos?userId={id} memakai AbortController, dengan jumlah todo yang selesai di bagian atas (hitung sebagai data turunan).
    Lihat solusi
    // 1.
    import { useEffect, useState } from 'react';
    
    export function PostList() {
      const [posts, setPosts] = useState([]);
      useEffect(() => {
        let ignore = false;
        async function load() {
          const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
          const data = await res.json();
          if (!ignore) setPosts(data);
        }
        load();
        return () => { ignore = true; };
      }, []);
      return <ol>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ol>;
    }
    
    // 2.
    import { useEffect, useState } from 'react';
    
    export function PostDetail({ postId }) {
      const [post, setPost] = useState(null);
      useEffect(() => {
        let ignore = false;
        fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
          .then((res) => res.json())
          .then((data) => { if (!ignore) setPost(data); });
        return () => { ignore = true; };
      }, [postId]);
      if (!post || post.id !== postId) return <p>Memuat...</p>;
      return <h2>{post.title}</h2>;
    }
    
    // 3.
    import { useEffect, useState } from 'react';
    
    export function TodoUser() {
      const [userId, setUserId] = useState(1);
      const [todos, setTodos] = useState([]);
    
      useEffect(() => {
        const controller = new AbortController();
        fetch(`https://jsonplaceholder.typicode.com/todos?userId=${userId}`, { signal: controller.signal })
          .then((res) => res.json())
          .then(setTodos)
          .catch((err) => { if (err.name !== 'AbortError') console.error(err); });
        return () => controller.abort();
      }, [userId]);
    
      const selesai = todos.filter((t) => t.completed).length; // data turunan
    
      return (
        <>
          <input type="number" min={1} max={10} value={userId} onChange={(e) => setUserId(Number(e.target.value))} />
          <p>{selesai} dari {todos.length} selesai</p>
          <ul>{todos.map((t) => <li key={t.id}>{t.completed ? '✅' : '⬜'} {t.title}</li>)}</ul>
        </>
      );
    }
    

    Ringkasan

    Kuis pemahaman

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