Tirai Transisi Halaman Imperatif
Layar transisi antar-rute yang disuntikkan langsung ke document.body agar tetap hidup dan menganimasikan progres selagi Next.js mengganti halaman di belakang layar.
Membaca sekitar 2 menit
Layar transisi antar-rute yang disuntikkan langsung ke document.body agar tetap hidup dan menganimasikan progres selagi Next.js mengganti halaman di belakang layar.
Hook usePageTransition membangun elemen #__ptc secara imperatif, menjalankan progres ke 75%, memanggil router.push, lalu memantau perubahan URL lewat requestAnimationFrame sebelum menutup tirai di 100%.
Menghindari efek kedipan putih atau perpindahan kasar saat berpindah dari kanvas undangan yang terkunci menuju halaman galeri.
Masalahnya dulu
Di adegan terakhir (Penutup), terdapat tombol 'Lihat Galeri Foto' yang mengarahkan tamu dari rute '/' menuju '/gallery'. Di Next.js, router.push bekerja secara asinkron; jika kita hanya mengandalkan state komponen di halaman asal untuk menampilkan animasi keluar, komponen tersebut akan keburu dihancurkan begitu rute berganti, menghasilkan perpindahan layar yang kasar.
Mengapa harus manipulasi DOM imperatif
Di dalam app/components/PageTransition.tsx, saya membangun hook usePageTransition yang menyuntikkan elemen tirai (#__ptc) beserta stylesheet-nya langsung ke document.body di luar pohon render React. Karena berada langsung di bawah body, tirai ini kebal terhadap proses unmount/mount halaman Next.js.
Ketika transisi harus bertahan melewati kematian halaman lama dan kelahiran halaman baru, tempat teraman bagi elemen transisi adalah langsung di document.body.
Implementasi di ngodingan ini
Saat navigate('/gallery') dipanggil, tirai menutup layar, menjalankan simulasi bar progres hingga 75%, memanggil router.push, lalu memantau kesiapan halaman baru menggunakan loop requestAnimationFrame:
router.push(href);
const navStart = Date.now();
const checkReady = () => {
const elapsed = Date.now() - navStart;
if (elapsed > 600 && window.location.pathname === href) {
if (intRef.current) { clearInterval(intRef.current); intRef.current = null; }
let cur = pctRef.current;
const finish = setInterval(() => {
cur = Math.min(100, cur + 6);
updatePct(cur);
if (cur >= 100) {
clearInterval(finish);
setTimeout(() => {
curtain.classList.remove("in");
curtain.classList.add("out");
}, 200);
}
}, 20);
return;
}
rafRef.current = requestAnimationFrame(checkReady);
};
rafRef.current = requestAnimationFrame(checkReady);Jebakan dan detail yang harus dijaga
- Menetapkan durasi tampil minimum 600ms (elapsed > 600) agar meskipun halaman galeri sudah ter-cache dan berpindah dalam 10ms, animasi nama mempelai di tirai tidak berkedip terlalu cepat.
- Menyediakan batas waktu darurat (fallback timeout) selama 3000ms yang otomatis membuka tirai kembali seandainya navigasi tertahan atau gagal, sehingga layar pengguna tidak pernah terkunci selamanya.
- Mengecek keberadaan elemen terlebih dahulu (document.getElementById('__ptc')) agar pemanggilan navigasi berikutnya cukup menggunakan ulang elemen DOM yang sudah ada tanpa membuat duplikat.