Skip to content

Entitas Terbit tapi Kosong di Pilar: Satu Opsi Cache di Fetch

Adityo Guni Waluyo

Entitas baru tak muncul di halaman pilar padahal API sudah mengembalikannya. Pelajaran satu opsi cache: no-store di fetch, ISR tetap dipertahankan.

Ringkasan

Gini ceritanya, kartu entitas baru gak mau muncul di halaman pilar padahal datanya udah Terbit dan API oke. Ternyata salahnya bukan di ISR, tapi fetch di getEntities gak pake opsi cache, jadi hasilnya nyangkut dari build time. Fixnya gampang, cuma tambah cache: no-store di situ, soalnya dev emang per-request jadi kejamanya gak keliatan.

Saya sedang jalan sesi parity audit di KotaPortal, tipe pekerjaan yang biasanya lima menit: bikin entitas lewat admin UI, set status Terbit, terus cek kartunya di halaman pilar. Kartunya tidak muncul.

Lapis yang saya telusuri berurutan: form submit di admin UI sukses, record sudah berstatus Terbit di database, dan API mengembalikan entitas itu, dicek dari dua tempat: langsung ke host dan dari dalam container, hasilnya identik. Respons GET /entities memuat entitas baru. Tapi grid entitas terbaru di halaman pilar tetap menampilkan yang lama. Yang bikin tambah menjengkelkan: di dev semuanya kelihatan sehat, entitas muncul begitu disimpan. Kebusukan datanya baru ketahuan saat regeneration di production.

Salah menuduh ISR

Tuduhan pertama saya ke ISR. Pikiran saya: pasti REVALIDATE_SECONDS kebesaran, atau route halaman pilar salah konfigurasi, jadi halaman menyajikan snapshot lama terus. Saya sempat mau menurunkan revalidate-nya ke nol supaya "pasti segar".

Padahal lapis halaman sudah benar dan memang sengaja begitu. Kontrak freshness-nya dipegang di level halaman: homepage memakai 300 detik, halaman lain 3600 detik, dan 0 di dev supaya setiap request kena regenerasi. Halaman pilar boleh stale sampai satu jam itu keputusan, bukan kecelakaan. Kalau saya paksa revalidate 0, yang berubah cuma cadence halaman, dan bug-nya tetap hidup.

Temuannya satu lapis lebih bawah

Kesalahannya ada di request-level data cache. getEntities di frontend/src/lib/api/endpoints.ts jalan tanpa opsi cache sama sekali. Di repo yang jalan di Next.js 14, fetch tanpa opsi di route yang diprarender statis diambil sekali saat build, lalu hasilnya nempel di data cache [1]. Dokumen fetch-nya eksplisit: "auto no cache (default): Next.js fetches the resource from the remote server on every request in development, but will fetch once during next build because the route will be statically prerendered" [1].

// sebelum: listing diambil sekali saat build
const res = await apiClient.GET("/entities")

// sesudah: ambil ulang tiap request, freshness dipegang halaman
const res = await apiClient.GET("/entities", { cache: "no-store" })

Satu baris cache: "no-store" itu yang akhirnya memberi kartu pada entitas Terbit. Definisi opsinya sendiri cocok dengan gejalanya: Next.js mengambil ulang resource dari server di setiap request [1].

Kenapa dev tidak menunjukkannya? Selain perilaku dev yang memang per-request [1], fetch yang dipulihkan dari Server Components HMR cache tidak dicatat di log sama sekali [4]. Bahkan dengan logging fetch dinyalakan, request itu bisa lewat tanpa jejak setelah HMR. Dev bukan tempat yang cukup untuk menguji freshness listing.

Dua kenop yang saling tidak tahu

Pelajarannya bukan "matikan cache di mana-mana". Dua mekanisme ini berdiri sendiri: opsi cache di fetch [2] dan export const revalidate di route.

export const revalidate = Number(process.env.REVALIDATE_SECONDS ?? 3600)

Kenop request mengatur apakah sebuah fetch menyimpan hasilnya. Kenop route mengatur kapan sebuah halaman diregenerasi. Kalau kontrak freshness sebuah listing dipegang halaman, request cache harus opt-out per call. Kalau tidak, data cache diam-diam menimpa cadence halaman, dan halaman pilar bisa stale bukan satu jam tapi sampai build berikutnya.

Soal versi, repo ini jalan di next ^14.2.0, era di mana default-nya masih menyimpan hasil fetch. Next.js 15 membalik default itu: "fetch requests are no longer cached by default. To opt specific fetch requests into caching, you can pass the cache: 'force-cache' option" [5]. Panduan caching sekarang juga menyatakan default-nya tidak di-cache, dengan force-cache sebagai opt-in [2]. Itu sebabnya opsi eksplisit lebih berharga daripada mengandalkan default framework: kontraknya menempel di call site, bukan di perilaku versi yang kebetulan dipakai. Jalur upgrade antarversinya sendiri terdokumentasi, termasuk perintah upgrade-nya [3].

Loop pembuktian yang saya tinggalkan: spec E2E parity yang bikin entitas dari UI, set Terbit, verifikasi row-nya, lalu assert kartunya muncul di halaman pilar. Regresi begini tidak boleh lagi lolos lewat "dev kelihatan baik". Satu modul yang tidak punya public surface saya catat sebagai keputusan pemilik, bukan bug yang dibetulkan diam-diam.

Sumber

Artikel terkait