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 localStorage atau sessionStorage: Objek window dan API web storage hanya eksis di browser. Jika komponen mengecek token dari localStorage saat 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 Cookie ke 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 (seperti onMounted) 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:

  1. 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.
  2. Inspeksi Melalui Nuxt DevTools: Buka panel Nuxt DevTools, navigasi ke tab State. Verifikasi bahwa state auth_user dan cookie auth_token sudah terisi sebelum proses hidrasi client dimulai.
  3. Uji Koneksi Hard Refresh (Bypass Cache): Tekan Ctrl + F5 (atau Cmd + 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 script entry.js dieksekusi.