Peringatan hydration mismatch merupakan salah satu masalah paling umum saat membangun aplikasi berbasis Server-Side Rendering (SSR) menggunakan Nuxt 3. Galat ini terjadi ketika pohon Virtual DOM (VDOM) yang dibuat oleh Vue pada browser (client mount) tidak identik dengan struktur HTML yang dihasilkan oleh server Node.js (SSR render). Pada fitur autentikasi, desinkronisasi ini memicu UI berkedip (flicker), tombol login yang terlambat berubah, atau kegagalan interaktivitas komponen.
Akar Masalah Desinkronisasi State Autentikasi
Penyebab utama ketidakcocokan hidrasi pada alur autentikasi Nuxt 3 berpusat pada ketiadaan akses data yang seragam antara lingkungan server dan client:
- Penggunaan
localStorageatausessionStorage: Objekwindowdan API web storage hanya eksis di browser. Jika komponen mengecek token darilocalStoragesaat render, server akan merender kondisi unauthenticated (tamu), sedangkan client langsung merender kondisi authenticated. Hasilnya: DOM diff langsung terpicu saat hidrasi. - Header Cookie Tidak Diteruskan ke SSR: Jika token disimpan dalam cookie tanpa konfigurasi path dan domain yang benar, atau jika permintaan internal server (SSR data fetching) tidak meneruskan header
Cookieke Nitro API engine, server menganggap permintaan tersebut anonim. - Perbedaan Eksekusi State Reaktif: Menggunakan
ref()global di luar cakupan fungsi composable menyebabkan cross-request state pollution antar pengguna di server, sementara inisialisasi state reaktif di dalam lifecycle hooks khusus client (sepertionMounted) mengubah DOM setelah server selesai merender markup.
Implementasi Composable Autentikasi SSR-Safe
Nuxt 3 menyediakan composable bawaan useCookie dan useState yang dirancang khusus untuk SSR. useCookie membaca header cookie HTTP saat proses SSR dan beralih membaca document.cookie di sisi browser secara transparan.
Hindari penggunaan ref() biasa untuk state autentikasi global. Gunakan useState untuk memastikan nilai diserialisasi ke dalam payload HTML (__NUXT__) dari server ke client tanpa kebocoran memori.
// composables/useAuth.ts
interface UserProfile {
id: string
name: string
role: string
}
export const useAuth = () => {
// useCookie membaca request headers di server dan document.cookie di client
const token = useCookie<string | null>('auth_token', {
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: false, // Ubah true jika token dikelola via HTTP-only proxy
maxAge: 60 * 60 * 24 * 7,
default: () => null,
})
// useState menjamin sinkronisasi payload server-ke-client
const user = useState<UserProfile | null>('auth_user', () => null)
const isAuthenticated = computed(() => Boolean(token.value))
const setAuth = (newToken: string, profile: UserProfile) => {
token.value = newToken
user.value = profile
}
const clearAuth = () => {
token.value = null
user.value = null
navigateTo('/auth/login')
}
return {
token,
user,
isAuthenticated,
setAuth,
clearAuth,
}
}Sinkronisasi Auth Header pada Server Engine (Nitro)
Ketika server Nuxt perlu memanggil endpoint internal API (Nitro routes) untuk memvalidasi sesi saat proses SSR berlangsung, header autentikasi dari browser asli harus diteruskan. Gunakan useRequestFetch() yang secara otomatis mewariskan header permintaan masuk, termasuk cookie:
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
const { isAuthenticated, user, clearAuth } = useAuth()
const fetchWithHeaders = useRequestFetch()
// Jalankan validasi profil jika token ada namun data user kosong
if (isAuthenticated.value && !user.value) {
try {
const profile = await fetchWithHeaders('/api/auth/me')
user.value = profile
} catch (error) {
clearAuth()
if (to.meta.requiresAuth) {
return navigateTo('/auth/login')
}
}
}
if (to.meta.requiresAuth && !isAuthenticated.value) {
return navigateTo('/auth/login')
}
})Pada endpoint Nitro, baca cookie secara langsung menggunakan utility internal H3:
// server/api/auth/me.get.ts
import { getCookie, createError } from 'h3'
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized: Cookie token missing',
})
}
// Validasi token dan kembalikan profil user
return {
id: 'usr_123',
name: 'Alex Developer',
role: 'admin',
}
})Penanganan UI Kondisional dan ClientOnly Fallback
Komponen antarmuka yang sangat bergantung pada identitas lokal pengguna (misalnya avatar profil, tombol login/logout, atau menu spesifik role) rentan menghasilkan hydration mismatch jika struktur fallback tidak identik.
Gunakan tag <ClientOnly> secara selektif pada elemen yang hanya bisa ditentukan di sisi client. Selalu sediakan slot #fallback dengan dimensi DOM yang sama untuk mencegah Cumulative Layout Shift (CLS):
<!-- components/NavbarAuth.vue -->
<template>
<nav class="flex items-center justify-between p-4">
<NuxtLink to="/" class="font-bold">AppLogo</NuxtLink>
<ClientOnly>
<div v-if="isAuthenticated" class="flex items-center gap-3">
<span>{{ user?.name }}</span>
<button @click="clearAuth" class="btn-logout">Keluar</button>
</div>
<div v-else class="flex items-center gap-2">
<NuxtLink to="/auth/login" class="btn-login">Masuk</NuxtLink>
</div>
<!-- Fallback mempertahankan layout DOM sebelum hidrasi selesai -->
<template #fallback>
<div class="h-8 w-24 animate-pulse rounded bg-gray-200" />
</template>
</ClientOnly>
</nav>
</template>
<script setup lang="ts">
const { isAuthenticated, user, clearAuth } = useAuth()
</script>Catatan Teknis: Penggunaan<ClientOnly>harus diperlakukan sebagai solusi untuk elemen non-SEO atau komponen yang dinamis secara client-side. Jangan membungkus seluruh halaman ke dalam<ClientOnly>karena hal tersebut menghapus keunggulan SSR sepenuhnya.
Metode Verifikasi dan Debugging
Untuk memastikan desinkronisasi telah teratasi sepenuhnya, lakukan pengujian berikut pada lingkungan development:
- Periksa Output Browser Console: Vue akan menampilkan peringatan spesifik:
[Vue warn]: Hydration node mismatch. Perhatikan tag HTML dan baris yang ditunjuk. Periksa perbedaan antara nilai teks SSR dan client. - Inspeksi Melalui Nuxt DevTools: Buka panel Nuxt DevTools, navigasi ke tab State. Verifikasi bahwa state
auth_userdan cookieauth_tokensudah terisi sebelum proses hidrasi client dimulai. - Uji Koneksi Hard Refresh (Bypass Cache): Tekan
Ctrl + F5(atauCmd + Shift + R) pada rute yang dilindungi. Periksa tab Network untuk memastikan HTML respon awal dari server sudah memuat struktur DOM yang sesuai dengan status login tanpa layout berkedip saat scriptentry.jsdieksekusi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!