Bab 24

Proyek Penutup: Dashboard Konsumsi API

Dua puluh tiga bab membangun potongan demi potongan. Bab ini menyatukannya. Tugasnya satu proyek: dashboard cuaca yang mengambil data dari API, menyimpan cache, dan merendernya sebagai daftar, dengan form untuk menambah dan menghapus kota.

Kuka berdiri di dapur membuka satu buku resep di atas meja, bahan-bahan mentah tertata berkelompok di talenan, dan satu piring menu lengkap sudah jadi di sisi lain meja.
Memasak satu menu lengkap dari teknik yang sudah dipelajari satu per satu.

Mengapa bab penutup berbentuk proyek

Mengetahui dan bisa memakai adalah dua hal berbeda. Contoh di setiap bab kecil dan sengaja dibuat kecil, jadi urutan penyusunan tidak pernah jadi soal. Proyek menanyakan hal yang tidak ditanyakan bab satuan. Potongan itu disusun dalam urutan apa. Siapa yang menyimpan data. Apa yang jalan duluan saat halaman dibuka.

Proyeknya dashboard cuaca. Dashboard adalah papan informasi yang mengumpulkan beberapa angka penting di satu layar, seperti dashboard mobil yang menyatukan kecepatan dan bahan bakar. Pilihan jatuh ke cuaca karena datanya nyata, gratis, tanpa kunci API, dan sudah pernah ditemu di bab 11 lewat API publik Open-Meteo.

Hitung bab yang terpakai. Form dan validasi dari bab 13 menambah kota. State dan localStorage dari bab 10 menyimpan daftarnya. fetch dan async dari bab 11 mengambil data. Perenderan daftar dari bab 12 menyusun kartu. Cache dari bab 23 membuat data lama dipakai ulang. Aksesibilitas bab 19 dan design token bab 20 ikut di markup dan warnanya. Hampir seluruh buku bekerja di satu halaman, dan justru itulah isi bab penutup.

Merencanakan proyek sebelum mengetik

Proyek selalu mulai dari daftar fitur, bukan dari kode. Fitur dashboard ini tiga. Pertama, menampilkan suhu terkini untuk setiap kota yang tersimpan. Kedua, menambah dan menghapus kota lewat form. Ketiga, saat halaman dibuka ulang, daftar kota tetap ada dan datanya tampil secepat mungkin.

Dari daftar fitur itu, alur data bisa digambar sebelum mengetik. Form mengirim nama kota. Daftar kota disimpan di localStorage. Setiap kota memicu satu permintaan fetch. Hasilnya dirender sebagai kartu. Urutan ini penting. Kalau tampilan ditulis duluan tanpa state yang jelas, halaman cepat jadi, lalu berantakan saat fitur berikutnya masuk. Data dulu, tampilan menyusul, pola yang sama dengan bab 10 dan bab 14.

Satu keputusan lingkup perlu disebut jujur. Kota yang boleh ditambah dibatasi enam pilihan dengan koordinat yang sudah tertulis di kode. Mengetik nama kota bebas memang lebih bebas, tetapi butuh API pencarian lokasi tambahan, dan fitur itu tidak dibayar kebutuhan bab ini. Batas yang disengaja lebih baik daripada lingkup yang melebar sendiri.

Alur data dari form ke layar

Semua perubahan lewat satu pintu. Ada satu fungsi render yang membaca state dan menggambar ulang daftar dari awal. Menambah kota berarti satu nama masuk state, lalu render dipanggil. Menghapus kota sama, satu nama keluar dari state. Tidak ada kode yang mengubah kartu satu per satu di tempat, karena daftar selalu digambar ulang dari state. Itulah cara bab 12 merender daftar, dipakai sungguhan.

Cache, minta sekali, pakai beberapa kali

Kuka membuka pintu kulkas berisi beberapa kotak makanan berlabel dan bertanggal, satu kotak dengan tanggal yang sudah lewat diangkatnya keluar.
Cache ala dapur, kotak berlabel tanggal, yang sudah lewat dibuang.

Setiap kartu butuh satu permintaan fetch, dan permintaan butuh waktu serta kuota. Cache, simpanan jawaban agar permintaan yang sama tidak diulang, sudah ditemu di bab 23. Aturannya dua langkah. Sebelum fetch, lihat cache. Ada datanya dan belum kedaluwarsa, pakai yang lama. Tidak ada atau sudah kedaluwarsa, minta yang baru lalu simpan.

Umur cache ditulis bersama datanya. localStorage menyimpan waktu jawaban diterima, dan setiap kali dibaca, selisihnya dihitung dengan Date.now(). Lebih dari sepuluh menit, data dianggap basi dan diminta ulang. Angka sepuluh menit tidak ada rumusnya, cukup sesuai cuaca yang berubah perlahan. Menghapus kota dari daftar juga ikut menghapus cache-nya, supaya kulkas tidak menumpuk kotak yang tidak diminta lagi.

Kondisi menunggu dan gagal

Jaringan tidak secepat kode. Selama menunggu, kartu menampilkan tulisan Mengambil, bukan kosong. Kalau permintaan gagal, kartu itu menampilkan pesan galat dan kota lain tetap aman, karena setiap kota mencoba dengan try/catch sendiri, pola bab 11. Daftar yang masih kosong juga punya tulisan, suruh menambah kota pertama. Halaman yang diam tanpa penjelasan terlihat seperti macet.

Batas proyek

Proyek ini sengaja kecil. Tidak ada grafik, tidak ada pembaruan otomatis, tidak ada login. Tujuan bab ini menyatukan yang sudah dipelajari, bukan menambah yang baru. Dashboard sungguhan akan menambah penyegaran berkala lewat cache bab 23, berpindah halaman tanpa muat ulang lewat bab 18, dan jaring pengaman lewat tes bab 22. Kalau pembaca mau melanjutkan proyek ini sendiri, tiga bab itu petanya.

Analogi: dapur

Satu gambar untuk semuanya. Proyek adalah memasak satu menu lengkap, bukan latihan memotong satu bahan. Data cuaca adalah bahan mentahnya, dan fetch adalah perjalanan ke pasar. Cache adalah kulkas, beli sekali, pakai beberapa kali, dan yang terlalu lama dibuang. State adalah catatan pesanan di meja dapur, sumber tunggal yang menentukan apa yang sedang dibuat. Render adalah menata piring, dan setiap pesanan baru menata ulang piring dari catatan, bukan mengubah satu piring di tempat. Bahan habis bukan bencana, cukup tulis di piring itu, pesanan lain tetap keluar.

Pasangan contoh

Contoh yang dipasangkan adalah unit intinya, satu kartu kota, dari ambil data sampai jadi tampilan. Vanilanya satu async function, cek cache, fetch kalau perlu, simpan cache bersama waktunya, lalu mengisi kartu dengan textContent. React-nya kartu yang sama sebagai komponen, data cuaca masuk state lewat useEffect, dan tampilan membaca state. Kode fetch dan cache-nya sama persis, yang berubah hanya tempat hasilnya dititipkan.

Vanilla JavaScript

const JEDA_CACHE = 10 * 60 * 1000;

function urlKota(nama) {
  const k = KOTA[nama]; // koordinat tiap kota
  return "https://api.open-meteo.com/v1/forecast"
    + "?latitude=" + k.lat + "&longitude=" + k.lon
    + "&current=temperature_2m";
}

async function kartuKota(nama, status) {
  const simpanan = JSON.parse(localStorage.getItem("cuaca-" + nama));
  if (simpanan && Date.now() - simpanan.waktu < JEDA_CACHE) {
    status.textContent = " " + simpanan.suhu + " °C (cache)";
    return;
  }
  try {
    const jawaban = await fetch(urlKota(nama));
    if (!jawaban.ok) throw new Error("status " + jawaban.status);
    const data = await jawaban.json();
    const baru = { suhu: data.current.temperature_2m, waktu: Date.now() };
    localStorage.setItem("cuaca-" + nama, JSON.stringify(baru));
    status.textContent = " " + baru.suhu + " °C";
  } catch (galat) {
    status.textContent = " gagal mengambil data";
  }
}

React

function KartuKota({ nama }) {
  const [status, setStatus] = React.useState("mengambil...");

  React.useEffect(() => {
    async function ambil() {
      const simpanan =
        JSON.parse(localStorage.getItem("cuaca-" + nama));
      if (simpanan && Date.now() - simpanan.waktu < JEDA_CACHE) {
        setStatus(simpanan.suhu + " °C (cache)");
        return;
      }
      const jawaban = await fetch(urlKota(nama));
      const data = await jawaban.json();
      const baru = { suhu: data.current.temperature_2m,
        waktu: Date.now() };
      localStorage.setItem("cuaca-" + nama, JSON.stringify(baru));
      setStatus(baru.suhu + " °C");
    }
    ambil().catch(() => setStatus("gagal mengambil data"));
  }, [nama]);

  return <p>{nama}: {status}</p>;
}

Perbedaan inti bab ini ada di sini, dan pendek. Logikanya sama persis, cek cache dulu, fetch kalau basi, simpan lagi. Vanilanya mengisi elemen langsung, dan urutannya jalan karena kedisiplinan penulis kode. React menaruh hasil fetch di state dan membiarkan tampilan mengikuti, slot yang sama dengan bab 16. Tidak ada satu pun konsep baru di kolom kanan. Setelah dua puluh tiga bab, itu kabar baiknya.

Demo Hidup

Demo di bawah menjalankan dashboard utuhnya. Pilih kota, tekan Tambah, satu kartu muncul dan mengambil suhunya dari server. Buka ulang halaman, daftarnya tetap ada dan datanya tampil seketika dari cache. Setelah sepuluh menit, angka diminta ulang. Tombol Hapus mengeluarkan kota dari daftar sekaligus dari cache.

Dashboard cuaca kecil. Data diambil dari API publik Open-Meteo, daftar kota dan cache disimpan di localStorage browser ini, dan daftar digambar ulang dari state setiap ada perubahan.

Begini cara baca demonya. Fungsi render membaca satu state, yaitu daftar kota, dan menggambar seluruh daftar dari awal setiap dipanggil. Form tidak pernah menambah kartu sendiri, ia hanya menambah nama ke state lalu memanggil render, dan validasinya satu, tolak kota yang sudah ada di daftar. Setiap kartu menitipkan pengambilan datanya ke kartuKota, yang mengecek cache di localStorage sebelum fetch, dan mencoba dengan try/catch sendiri sehingga satu kota gagal tidak menjatuhkan yang lain. Waktu jawaban disimpan bersama suhunya, dan selisihnya dengan Date.now() yang menentukan basi atau tidak.

Ringkasan bab ini, sekaligus buku ini. Proyek menyatukan potongan-potongan jadi satu halaman yang bekerja, dengan alur yang satu arah. Form mengubah state, state tersimpan di localStorage, render menggambar ulang dari state, dan setiap kota mengambil datanya sendiri dengan cache di depannya. Tidak ada teknik baru di bab ini, dan justru itu ujiannya. Kalau alur ini terbaca jelas, seluruh isi buku sudah sampai. Proyek ini titik mulai, bukan garis akhir.

Rujukan