Akar Masalah Hydration Mismatch pada Nilai BigInt

Error hydration mismatch terjadi saat pohon DOM yang dihasilkan server berbeda dengan DOM dari render pertama di client. Pada aplikasi yang menampilkan metrik berdurasi ekstrem atau hitungan astronomis seperti proyek Until_Heat_Death_Do_Us_Part, counter membutuhkan integer 64-bit hingga 128-bit via tipe data primitif BigInt.

Dua faktor utama memicu kegagalan hydration pada kasus ini:

  1. Keterbatasan Serialisasi JSON Standar: Spesifikasi ECMAScript melarang JSON.stringify() memproses BigInt secara langsung. Mencoba menserialisasi data state SSR yang memuat BigInt menghasilkan runtime exception TypeError: Do not know how to serialize a BigInt. Konversi manual yang tidak seragam antara server dan client memicu perbedaan string representation.
  2. Desinkronisasi Streaming State: Counter yang bergantung pada stream network (misalnya Server-Sent Events atau raw socket broadcast) terus berdetak. Saat server me-render HTML pada timestamp $T_0$, payload dikirim lewat jaringan. Ketika bundle JavaScript dieksekusi di browser pada $T_0 + \Delta t$, counter client telah menerima tick baru atau membaca clock lokal yang berbeda, sehingga initial client render tidak cocok dengan HTML SSR.

Penanganan Serialisasi BigInt pada Boundary SSR

Ekstensi protokol serialisasi diperlukan agar state awal dari server dapat dikonsumsi client tanpa distorsi tipe data atau presisi float.

// lib/serializer.ts
export function serializeWithBigInt(data: unknown): string {
  return JSON.stringify(data, (_, value) =>
    typeof value === 'bigint' ? `${value.toString()}n` : value
  );
}

export function parseWithBigInt(text: string): unknown {
  return JSON.parse(text, (_, value) => {
    if (typeof value === 'string' && /^-?\d+n$/.test(value)) {
      return BigInt(value.slice(0, -1));
    }
    return value;
  });
}

Pendekatan string berakhiran n menjaga integritas nilai 64-bit dan mencegah integer overflow yang terjadi jika dipaksa masuk ke tipe Number IEEE-754 standar (batas aman: Number.MAX_SAFE_INTEGER = 9,007,199,254,740,991).

Sinkronisasi Stream via useSyncExternalStore

Penggunaan useEffect untuk mengikat subscription stream sering memicu dua kali render atau layout shift. React menyediakan API useSyncExternalStore untuk mengelola external store konkuren secara deterministik antara SSR dan client hydration.

Metode ini mewajibkan deklarasi getServerSnapshot yang menghasilkan nilai statis deterministik untuk fase SSR, serta getSnapshot untuk membaca nilai live client tanpa memicu hydration bailout.

// hooks/useBigIntStream.ts
import { useSyncExternalStore } from 'react';

type Listener = () => void;

class CounterStreamStore {
  private value: bigint;
  private listeners = new Set<Listener>();

  constructor(initialValue: bigint = 0n) {
    this.value = initialValue;
  }

  subscribe = (listener: Listener) => {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  };

  getSnapshot = (): bigint => {
    return this.value;
  };

  update = (next: bigint) => {
    this.value = next;
    this.listeners.forEach((l) => l());
  };
}

export const counterStore = new CounterStreamStore(0n);

export function useBigIntCounter(serverFallback: bigint) {
  return useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getSnapshot,
    () => serverFallback // getServerSnapshot: menjamin output SSR deterministik
  );
}

Isolasi Live Widget via Dynamic Import Tanpa SSR

Komponen counter yang menerima puluhan update per detik dari stream broadcast jaringan tidak perlu di-render secara parsial di server jika nilai awal langsung invalidated seketika. Solusi paling bersih untuk widget real-time adalah mengisolasi rendering dari siklus SSR.

// components/LiveCounterWidget.tsx
'use client';

import { useBigIntCounter } from '@/hooks/useBigIntStream';

export default function LiveCounterWidget({ fallbackValue }: { fallbackValue: string }) {
  const count = useBigIntCounter(BigInt(fallbackValue));

  return (
    <div className="counter-display">
      <span>Current Tick:</span>
      <strong>{count.toString()}</strong>
    </div>
  );
}

Konsumsi komponen tersebut pada Server Component menggunakan opsi ssr: false untuk menghentikan evaluasi pohon DOM counter pada server execution context:

// app/page.tsx
import dynamic from 'next/dynamic';

const LiveCounterWidget = dynamic(
  () => import('@/components/LiveCounterWidget'),
  {
    ssr: false,
    loading: () => <span>Menghubungkan ke broadcast stream...</span>
  }
);

export default function Page() {
  const initialValue = "18446744073709551615"; // Contoh BigInt 64-bit

  return (
    <main>
      <h1>Monitor Metrik Temporal</h1>
      <LiveCounterWidget fallbackValue={initialValue} />
    </main>
  );
}

Aturan Debugging dan Verifikasi

Saat menelusuri mismatch error pada browser console:

  • Cek Atribut Terminal: Jika error menampilkan perbedaan text content murni (contoh: Server: 1000n, Client: 1001n), masalah ada pada sinkronisasi timing render awal. Solusi: Gunakan getServerSnapshot yang identik dengan static seed dari server layout.
  • Cek String vs BigInt Type coercion: Hindari pemanggilan .toLocaleString() langsung pada komponen server tanpa mengunci locale numerik spesifik. Format representasi angka sistem server (Node.js/Linux) dapat berbeda dengan runtime browser client.
  • Bypass vs Supress: Jangan gunakan suppressHydrationWarning untuk menyembunyikan inkonsistensi data BigInt. Atribut tersebut hanya menekan peringatan di console tanpa menyelesaikan root cause desinkronisasi arsitektur data.