Menyisipkan pengecekan typeof window !== 'undefined' langsung di dalam render tree JSX adalah salah satu saran keliru yang sering dihasilkan oleh AI coding assistant. Kode tersebut memang menyelesaikan masalah crash "window is not defined" di sisi server secara instan, namun langsung menciptakan masalah baru yang lebih berbahaya: hydration mismatch.

Akar Masalah: Mengapa typeof window Merusak Rehidrasi

Server-Side Rendering (SSR) pada framework modern seperti Next.js atau Nuxt bekerja melalui dua fase: fase render HTML di server, dan fase rehidrasi di browser. Rehidrasi adalah proses di mana React atau runtime framework berjalan di client, membangun Virtual DOM (VDOM) lokal, lalu menempelkannya ke DOM statis yang sudah dikirim oleh server.

Kunci keberhasilan rehidrasi adalah konsistensi: VDOM yang dihasilkan pada render pertama di client harus identik secara struktural dengan HTML yang dihasilkan oleh server.

Ketika Anda menulis kode seperti ini:

// Anti-pattern: percabangan typeof window langsung di render tree
function UserHeader() {
  if (typeof window !== 'undefined') {
    return <UserMenu token={localStorage.getItem('token')} />;
  }
  return <LoginButton />;
}

Eksekusi kode tersebut memecah determinisme pohon tampilan:

  • Di Server (Node.js runtime): window bernilai undefined. Server merender <LoginButton /> ke dalam HTML.
  • Di Client (Browser runtime): window terdefinisi. Client merender <UserMenu /> pada evaluasi render pertama.

Anatomi Kegagalan Rehidrasi

Ketika React mendeteksi perbedaan antara DOM riil hasil SSR dan VDOM client, proses penautan (attachment) internal gagal. Tiga kegagalan teknis terjadi secara berurutan:

1. DOM Desynchronization dan Event Listener yang Hilang

React tidak membangun ulang seluruh DOM dari nol saat rehidrasi. Framework mengasumsikan node DOM di HTML cocok satu banding satu dengan VDOM untuk menghemat alokasi memori. Jika server mengirim tag <button> tetapi client menghasilkan <div>, pointer event listener bisa terpasang pada node DOM yang salah atau bahkan tidak terpasang sama sekali. Pengguna melihat tombol di layar, tetapi aksi klik tidak memicu fungsi apa pun.

2. Layout Shift (CLS) dan Thrashing Renderer

Ketika ketidakcocokan terdeteksi, React terpaksa membuang (unmount) node server yang salah dan menyisipkan node baru dari client. Operasi penghapusan dan penyisipan node DOM ini memicu kalkulasi ulang gaya (recalculate styles) dan relayout halaman secara instan. Ini berdampak langsung pada metrik Cumulative Layout Shift (CLS).

3. Memory Waste

Manfaat SSR adalah mempercepat First Contentful Paint (FCP) dengan menyajikan HTML siap pakai. Perombakan node akibat mismatch membatalkan penghematan resource tersebut karena browser harus menjalankan parsing HTML, mengeksekusi JS, menghapus DOM lama, dan merender ulang elemen dari awal.

suppressHydrationWarning: Menutupi Bug Arsitektur

Atribut suppressHydrationWarning sering disalahgunakan sebagai jalan pintas untuk membungkam peringatan merah di console browser. Atribut ini dirancang hanya untuk teks yang perbedaannya tidak bisa dihindari, seperti stempel waktu lokal (timestamp) atau hash acak.

Peringatan: Menaruh suppressHydrationWarning pada komponen yang memiliki cabang logika struktural tidak memperbaiki kerusakan event listener atau layout shift. Fitur ini hanya mematikan log di konsol, meninggalkan bug interaktivitas tetap aktif di lingkungan produksi.

3 Solusi Praktis Menggantikan Percabangan window

1. Gunakan Native CSS Media Query Menggantikan window.innerWidth

Kebutuhan menampilkan komponen berbeda berdasarkan ukuran layar tidak memerlukan objek window di JavaScript. Manfaatkan fitur native platform.

/* Native CSS: Nol mismatch, nol overhead JS */
.mobile-nav {
  display: block;
}
.desktop-nav {
  display: none;
}

@media (min-width: 768px) {
  .mobile-nav {
    display: none;
  }
  .desktop-nav {
    display: block;
  }
}
// Kedua elemen dirender di server dan client secara identik.
// CSS menangani visibilitas tanpa memicu hydration mismatch.
function Navigation() {
  return (
    <>
      <div className="mobile-nav"><MobileMenu /></div>
      <div className="desktop-nav"><DesktopMenu /></div>
    </>
  );
}

2. Two-Pass Rendering Menggunakan useEffect Terisolasi

Jika komponen wajib mengakses browser API (seperti localStorage) sebelum menentukan outputnya, tunda rendering komponen spesifik browser hingga fase hidrasi selesai.

import { useState, useEffect } from 'react';

function ClientOnlyBadge() {
  const [isMounted, setIsMounted] = useState(false);

  // useEffect hanya berjalan di client SETELAH rehidrasi awal selesai
  useEffect(() => {
    // ponytail: upgrade to cookie/session headers if avoiding flash-of-unstyled-content is required
    setIsMounted(true);
  }, []);

  if (!isMounted) {
    // Output fallback identik antara server dan initial client render
    return <span className="badge-placeholder" />;
  }

  return <span>{window.navigator.userAgent}</span>;
}

3. Dynamic Import Tanpa SSR untuk Widget Murni Client

Untuk komponen pihak ketiga yang mengandalkan objek window secara masif (misalnya chart Canvas atau editor WYSIWYG), kecualikan komponen tersebut sepenuhnya dari proses server render.

import dynamic from 'next/dynamic';

// Nonaktifkan kompilasi server untuk komponen yang bergantung pada browser DOM
const HeavyChartWidget = dynamic(
  () => import('./HeavyChartWidget'),
  { 
    ssr: false,
    loading: () => <div className="h-64 animate-pulse bg-gray-100" />
  }
);

Metode ini memastikan server hanya merender skeleton placeholder, client melakukan hidrasi placeholder tersebut tanpa konflik, lalu komponen utama diunduh dan dimuat setelah siklus rehidrasi tuntas.