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:
- Server Run: Header request membaca cookie auth yang valid (misal:
access_token). Server me-render komponen<DashboardLayout />beserta daftar node proxy aktif. - Client Run (Initial Pass): Browser mengunduh bundle JavaScript. Sesi di client belum siap membaca async store atau
localStorage(nilai awal masihnullatauisAuthenticated: false). Komponen mengembalikan<LoginForm />. - 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
localStorageuntuk menentukan apakah komponen UI login atau layout dashboard yang harus ditampilkan pertama kali. - Gunakan Cookie
SameSite=LaxatauStrict: Untuk dashboard proxy internal, konfigurasi flagSecuredanHttpOnlypada 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!