Hydration error pada dashboard manajemen proxy (seperti arsitektur panel berbasis Marzban atau X-UI) umumnya terjadi akibat inkonsistensi pembacaan state autentikasi. Server me-render tree DOM berdasarkan cookie HTTP, sedangkan browser mengevaluasi sesi lewat localStorage atau state manager asinkron. Perbedaan ini memicu layout flicker, form login ter-render ganda sesaat, dan rusaknya struktur virtual DOM.

Akar Masalah: Dual-State Evaluation pada SSR

Saat request masuk ke node SSR, server hanya memiliki akses ke HTTP request headers (termasuk header Cookie). Di sisi lain, aplikasi client sering kali menyimpan token autentikasi di localStorage atau memicu validasi token via REST API asinkron di dalam hook useEffect atau onMounted.

Skenario mismatch terjadi sebagai berikut:

  1. Server Run: Header request membaca cookie auth yang valid (misal: access_token). Server me-render komponen <DashboardLayout /> beserta daftar node proxy aktif.
  2. Client Run (Initial Pass): Browser mengunduh bundle JavaScript. Sesi di client belum siap membaca async store atau localStorage (nilai awal masih null atau isAuthenticated: false). Komponen mengembalikan <LoginForm />.
  3. Hydration Bailout: Virtual DOM client tidak cocok dengan DOM hasil render server. Engine UI memunculkan peringatan Hydration failed because the initial UI does not match what was rendered on the server dan memicu render ulang paksa yang merusak UX.

Solusi: Deterministik SSR Token Injection

Solusi paling stabil adalah menjadikan cookie httpOnly sebagai single source of truth saat server-side render, lalu menyuntikkan status tersebut ke client state tree sebagai initial payload tanpa bergantung pada localStorage.

1. Pola Layout Wrapper Deterministik

Gunakan arsitektur state injection via context atau root provider yang menerima state awal langsung dari eksekusi server (contoh implementasi React/Next.js atau kerangka SSR sejenis):

// auth-context.jsx
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext({
  isAuthenticated: false,
  user: null,
  token: null,
});

export function AuthProvider({ initialAuth, children }) {
  // Inisialisasi state langsung dari payload SSR, hindari default null jika server me-render authenticated UI
  const [auth] = useState(initialAuth);

  return (
    <AuthContext.Provider value={auth}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

2. Ekstraksi Cookie Server dan Page Wrapping

Server membaca cookie auth dan membungkus layout secara konsisten sebelum markup dikirimkan ke client:

// proxy-dashboard-page.jsx
import { cookies } from 'next/headers'; // atau parser cookie standar
import { AuthProvider } from './auth-context';
import ProxyNodeList from './proxy-node-list';
import LoginForm from './login-form';

export default async function DashboardPage() {
  const cookieStore = cookies();
  const token = cookieStore.get('access_token')?.value || null;

  // Verifikasi token sederhana di server edge / SSR layer
  const isAuthenticated = Boolean(token);
  const initialAuth = {
    isAuthenticated,
    token,
    user: isAuthenticated ? { role: 'admin' } : null,
  };

  return (
    <AuthProvider initialAuth={initialAuth}>
      {isAuthenticated ? <ProxyNodeList /> : <LoginForm />}
    </AuthProvider>
  );
}

Pengujian Validitas State dengan Runnable Assert

Pastikan state parser menghasilkan output identik di server dan client untuk payload yang sama. Jalankan skrip validasi native Node.js berikut tanpa library eksternal:

// verify-auth-sync.js
const assert = require('assert');

function parseAuthState(cookieHeader, clientStoreToken) {
  // Simulasi parsing cookie server
  const serverCookies = (cookieHeader || '').split(';').reduce((acc, str) => {
    const [k, v] = str.trim().split('=');
    if (k && v) acc[k] = decodeURIComponent(v);
    return acc;
  }, {});

  const serverToken = serverCookies['access_token'] || null;

  // Sync logic: prioritaskan serverToken untuk initial pass
  const resolvedToken = serverToken ?? clientStoreToken ?? null;
  const isAuthenticated = resolvedToken !== null;

  return {
    isAuthenticated,
    token: resolvedToken
  };
}

// Case 1: SSR Cookie tersedia, client storage kosong (initial hydration pass)
const serverRender = parseAuthState('access_token=marzban_jwt_secret', null);
const clientHydrate = parseAuthState('access_token=marzban_jwt_secret', null);

assert.strictEqual(
  serverRender.isAuthenticated,
  clientHydrate.isAuthenticated,
  'FAIL: Status autentikasi harus identik antara SSR dan Client pass'
);
assert.strictEqual(
  serverRender.token,
  clientHydrate.token,
  'FAIL: Token harus ter-resolve identik untuk menghindari mismatch'
);

// Case 2: Tidak ada cookie auth
const unauthRender = parseAuthState('', null);
assert.strictEqual(unauthRender.isAuthenticated, false);

console.log('PASS: Auth synchronization logic deterministik.');

Jalankan uji coba di terminal: node verify-auth-sync.js. Skrip keluar dengan status 0 jika sinkronisasi deterministik.

Tips Mitigasi

  • Hapus ketergantungan localStorage untuk rendering awal: Jangan gunakan item dari localStorage untuk menentukan apakah komponen UI login atau layout dashboard yang harus ditampilkan pertama kali.
  • Gunakan Cookie SameSite=Lax atau Strict: Untuk dashboard proxy internal, konfigurasi flag Secure dan HttpOnly pada cookie JWT agar tidak dapat diakses skrip XSS pihak ketiga dan selalu terkirim pada navigasi level atas.
  • Two-Pass Rendering jika terpaksa: Jika data client-only benar-benar dibutuhkan, biarkan initial pass menampilkan UI netral (skeleton layout), lalu mutasikan UI setelah komponen ter-mount di client via flag mounted === true.