Katalog toko Budi punya tiga tab penyaring: Semua, Terlaris, dan Promo. Klik pada tab mengganti isi panel lewat JavaScript — cepat, tanpa memuat ulang halaman. Persoalannya muncul dari dua arah: tombol kembali browser tidak mengembalikan tab sebelumnya, melainkan langsung keluar dari toko, dan menyegarkan halaman mengembalikan tampilan ke tab Semua. Browser menganggap URL tidak pernah berubah, karena memang belum berubah — kita baru mengganti tampilan.
History API menutup celah itu: history.pushState() menambahkan entri riwayat baru sekaligus mengubah URL di address bar, tanpa memuat ulang halaman sekalipun. Ditambah event popstate yang dikirim ketika pengguna berpindah riwayat, kita memiliki semua bahan untuk navigasi aplikasi satu halaman (SPA). Chapter ini membahas ketiganya beserta konsep routing SPA yang dibangun di atasnya.
Mengubah URL Tanpa Memuat Ulang #
Aturannya: history.pushState(state, judul, url) menerima tiga argumen — object state yang kita simpan bersama entri riwayat, judul (saat ini diabaikan browser, umumnya diisi string kosong), dan URL baru pada origin yang sama. Berkas berikut merangkai tab katalog dengan riwayat:
// navigasi-spa.js — mengganti tampilan dan URL tanpa memuat ulang halaman
const panel = document.querySelector("#panel");
function buka(nama, url) {
panel.textContent = `Katalog: ${nama}`;
history.pushState({ halaman: nama }, "", url);
console.log(`Tampilan: ${nama} | URL: ${location.pathname}`);
}
// dua tautan katalog; klik dari pengunjung memanggil buka()
document.querySelector("#terlaris").addEventListener("click", () => {
buka("Terlaris", "/katalog/terlaris");
});
document.querySelector("#promo").addEventListener("click", () => {
buka("Promo", "/katalog/promo");
});
// tombol kembali/maju (dan navigasi riwayat lainnya) memicu popstate;
// saat event ini tiba, location sudah menunjuk entri yang dipulihkan
window.addEventListener("popstate", (event) => {
const nama = event.state ? event.state.halaman : "Semua";
panel.textContent = `Katalog: ${nama}`;
console.log(`Kembali ke: ${nama}`);
});Keluaran berikut dicatat dari eksekusi nyata berkas ini di lingkungan DOM jsdom. Dua baris pertama dihasilkan dengan mensimulasikan klik pada kedua tautan; baris ketiga dari pengiriman event popstate secara terprogram (new PopStateEvent(...)) untuk mensimulasikan tombol kembali — jsdom tidak menjalankan penelusuran riwayat, sedangkan di browser nyata tombol kembali/maju adalah pemicunya:
Output yang Dihasilkan (console browser):
Tampilan: Terlaris | URL: /katalog/terlaris
Tampilan: Promo | URL: /katalog/promo
Kembali ke: TerlarisMari kita telaah baris-baris pentingnya:
- Baris 5–9 (
function buka): satu function melayani semua tab. Urutannya penting — tampilan diganti lebih dulu, lalupushStatemencatat entri riwayat, kemudian URL terbaru dicetak. - Baris 7 (
history.pushState(...)): state{ halaman: nama }disimpan bersama entri riwayat, dan URL panel berubah menjadi/katalog/terlarisatau/katalog/promo. Halaman tidak dimuat ulang; hanya address bar dan entri riwayat yang diperbarui. - Baris 12–17 (listener klik): setiap tautan membawa URL tujuannya sendiri. Di halaman sungguhan kita juga memanggil
event.preventDefault()pada tautan agar browser tidak menavigasi secara penuh — pola yang sama dengan chapter Preventing Default Behavior. - Baris 21–25 (listener
popstate): ketika pengguna menekan tombol kembali, browser memulihkan URL dan mengirimpopstatedengan state entri tersebut dievent.state. Kode di sini yang bertugas menggambar ulang tampilan — browser tidak melakukannya untuk kita. Fallback: "Semua"menangani entri awal yang tidak punya state.
Setelah pushState berjalan, dua tempat menyimpan informasi yang sama: location.pathname berisi URL baru, dan history.state berisi state entri aktif — { halaman: "Terlaris" } pada contoh di atas. Keduanya bisa dibaca kapan saja, tidak hanya saat event datang.
Object state disimpan browser dengan mekanisme penyalinan mendalam (structured clone): object biasa, array, string, angka, hingga null aman disertakan. Yang tidak boleh ikut adalah nilai yang tidak bisa disalin — function, node DOM, atau class instance dengan method-nya. Bila terpaksa menyertakan, simpan datanya saja (misalnya id produk) dan rujuk sisa informasinya lewat kunci tersebut. Batasan ini menjaga state tetap dapat diserialisasi antar muat halaman: ketika pengunjung menekan tombol kembali setelah berminggu-minggu, browser memulihkan state dari penyimpanan riwayatnya, bukan dari memori halaman yang sudah hilang.
pushState, replaceState, dan popstate #
History API memiliki dua method penulisan, dan perbedaannya menentukan bentuk riwayat:
| Method | Akibat pada riwayat |
|---|---|
pushState(state, judul, url) | menambah entri baru; tombol kembali kembali ke tampilan sebelumnya |
replaceState(state, judul, url) | mengganti entri saat ini; tombol kembali melewati URL yang diganti |
location.href = url | navigasi penuh — halaman dimuat ulang, seluruh state JavaScript hilang |
pushState dipakai untuk setiap langkah yang ingin bisa dibalik pengguna. replaceState cocok untuk pembaruan yang bukan langkah baru: mengganti ?halaman=2 saat pengguna menggulir, atau menyimpan urutan penyaringan terakhir. Pemakaian replaceState untuk langkah yang seharusnya berdiri sendiri membuat tombol kembali melewatkannya — pengguna yang ingin kembali satu langkah justru terlempar dua langkah.
Tentang event popstate, tiga fakta menentukan pemakaiannya yang benar. Pertama, pushState dan replaceState tidak memicu popstate — event ini khusus untuk perpindahan antar entri riwayat yang dilakukan browser (tombol kembali, maju, history.back(), history.forward()). Kedua, ketika event tiba, URL halaman sudah diperbarui lebih dulu; kode di dalam listener membaca location yang sudah menjadi tujuan. Ketiga, selain state, perpindahan riwayat antar URL yang hanya berbeda hash-nya juga melalui jalur yang sama — dan bagi kebutuhan itu tersedia pula event khusus hashchange.
Satu properti history lagi yang sering berguna pada SPA: history.scrollRestoration. Secara default browser memulihkan posisi gulir saat berpindah entri ("auto"); aplikasi yang mengelola gulirnya sendiri — misalnya menggulir ke atas setiap ganti rute — menyetelnya ke "manual" dan mengatur posisi gulir di dalam kode. Mengabaikannya menghasilkan perilaku ganda: browser dan kode sama-sama menggeser guliran.
Konsep Routing SPA #
Rangkaian di atas — tampilkan, catat riwayat, pulihkan dari popstate — adalah routing aplikasi satu halaman dalam bentuk paling murni. Satu berkas HTML dimuat sekali; setiap perpindahan halaman dikerjakan JavaScript: mendengarkan klik tautan, memanggil pushState, dan menggambar tampilan baru; popstate menangani tombol kembali dan maju. Router pada framework modern memakai mekanisme yang sama persis di bawah library-nya — hanya ditambah pencocokan pola seperti /buku/:id dan pengelolaan data per rute.
Dua keputusan desain menentukan mutu routing buatan sendiri. Pertama, sumber kebenaran tampilan: pada contoh di atas, event.state.halaman yang menentukan apa yang digambar, bukan variabel global yang mudah meleset. Kedua, kelengkapan pemulihan: setiap URL yang bisa dicapai pushState harus menghasilkan tampilan yang sama bila dibuka langsung — karena pengunjung bisa membagikan tautan itu kapan saja. Kedua aturan ini kita kenal lagi sebagai prinsip “URL adalah state” pada section Architecture dan Patterns; di sana pola routing dibawa ke arsitektur aplikasi yang lebih besar.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
navigasi-spa.js
Unduh berkas// navigasi-spa.js — mengganti tampilan dan URL tanpa memuat ulang halaman
const panel = document.querySelector("#panel");
function buka(nama, url) {
panel.textContent = `Katalog: ${nama}`;
history.pushState({ halaman: nama }, "", url);
console.log(`Tampilan: ${nama} | URL: ${location.pathname}`);
}
// dua tautan katalog; klik dari pengunjung memanggil buka()
document.querySelector("#terlaris").addEventListener("click", () => {
buka("Terlaris", "/katalog/terlaris");
});
document.querySelector("#promo").addEventListener("click", () => {
buka("Promo", "/katalog/promo");
});
// tombol kembali/maju (dan navigasi riwayat lainnya) memicu popstate;
// saat event ini tiba, location sudah menunjuk entri yang dipulihkan
window.addEventListener("popstate", (event) => {
const nama = event.state ? event.state.halaman : "Semua";
panel.textContent = `Katalog: ${nama}`;
console.log(`Kembali ke: ${nama}`);
});