Audit hydration React menjadi semakin penting saat aplikasi mulai memakai UI AI, personalisasi, atau komponen context-aware yang berubah berdasarkan kondisi browser, locale, state lokal, atau hasil inferensi. Masalahnya sederhana: HTML yang dihasilkan saat SSR harus cocok dengan render pertama di browser. Jika berbeda, React dapat menampilkan peringatan hydration mismatch, membuang subtree tertentu, atau memaksa render ulang di client.
Pada aplikasi Next.js App Router, mismatch sering muncul bukan karena bug besar, melainkan karena detail kecil yang tidak deterministik: akses window terlalu awal, pembacaan localStorage saat render, format tanggal yang bergantung timezone, feature detection di server, atau state awal yang tidak identik. Dalam konteks tren UI berbasis AI dan eksperimen seperti demo webMCP yang memperlihatkan antarmuka web makin dinamis, prinsip dasarnya tetap sama: render pertama harus stabil dan dapat diprediksi.
Artikel ini fokus pada cara mengaudit dan memperbaiki mismatch tersebut secara praktis, khususnya pada React/Next.js dengan App Router.
Mengapa hydration mismatch terjadi?
Saat SSR, server mengirim HTML awal. Di browser, React melakukan hydration: ia memasang event handler dan mencocokkan tree virtual dengan DOM yang sudah ada. Jika output render pertama di client berbeda dari HTML server, React mendeteksi ketidaksesuaian.
Pada UI AI, mismatch lebih mudah terjadi karena tampilan sering dipengaruhi konteks runtime, misalnya:
- Preferensi pengguna yang hanya tersedia di browser.
- Hasil eksperimen atau model yang dijalankan setelah halaman dimuat.
- Deteksi kemampuan perangkat, viewport, atau media query.
- Konten yang berubah karena stream, suspense, atau fetch tambahan.
Prinsip pentingnya: SSR dan render pertama di client harus menggunakan input yang sama. Jika input berbeda, output juga akan berbeda.
Penyebab umum hydration mismatch pada React/Next.js
1. Data non-deterministik saat render
Contoh paling umum adalah memakai Date.now(), new Date(), Math.random(), atau ID acak langsung di fungsi komponen. Nilai tersebut bisa berbeda antara server dan client, bahkan dalam selang milidetik.
// Salah: nilai berubah antara SSR dan client render pertama
export default function Banner() {
return <p>Generated at: {Date.now()}</p>
}Jika memang perlu menampilkan waktu saat halaman dibuka, kirim nilainya dari server sebagai prop, atau render placeholder stabil lalu isi setelah mount.
// Benar: nilai SSR dikunci dari server
export default function Banner({ generatedAt }: { generatedAt: number }) {
return <p>Generated at: {generatedAt}</p>
}Alternatif lain, jika nilainya memang harus berasal dari browser, jangan render nilainya pada SSR.
'use client'
import { useEffect, useState } from 'react'
export default function ClientTime() {
const [time, setTime] = useState<number | null>(null)
useEffect(() => {
setTime(Date.now())
}, [])
return <p>Generated at: {time ?? '...'}</p>
}2. Akses window, document, localStorage saat render
Server tidak memiliki window atau localStorage. Masalahnya bukan hanya error runtime; sering kali developer menambahkan guard seperti typeof window !== 'undefined' langsung saat render, lalu menghasilkan output yang berbeda di server dan client.
// Salah: output server dan client bisa berbeda
'use client'
export default function ThemeLabel() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme')
: 'light'
return <span>Theme: {theme}</span>
}Di server, hasilnya light. Di client, hasilnya mungkin dark. Ini mismatch klasik.
Perbaiki dengan state awal yang stabil, lalu baca browser API setelah mount:
'use client'
import { useEffect, useState } from 'react'
export default function ThemeLabel() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = window.localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <span>Theme: {theme}</span>
}Jika perubahan dari light ke dark menyebabkan flicker yang tidak diinginkan, pertimbangkan mengirim preferensi tema dari cookie pada request sehingga server dan client punya input awal yang sama.
3. Locale dan timezone berbeda
Format tanggal, angka, mata uang, dan bahasa dapat berbeda antara server dan browser. Ini sering terjadi jika server memakai locale default tertentu, sedangkan browser pengguna memakai locale lain.
// Berisiko mismatch jika locale/timezone tidak identik
export default function Price({ amount }: { amount: number }) {
return <span>{amount.toLocaleString()}</span>
}Lebih aman jika Anda:
- Menentukan locale secara eksplisit.
- Menghindari formatting berbasis timezone lokal pada SSR jika timezone pengguna belum diketahui.
- Mengirim locale/timezone dari request, cookie, atau preferensi yang tersimpan.
// Lebih stabil karena locale eksplisit
export default function Price({ amount }: { amount: number }) {
return <span>{new Intl.NumberFormat('id-ID').format(amount)}</span>
}Untuk waktu lokal pengguna, strategi yang umum adalah merender format netral saat SSR, lalu mengganti ke versi lokal setelah mount jika memang dibutuhkan.
4. Feature detection saat render
UI AI atau context-aware UI sering bercabang berdasarkan ukuran layar, touch capability, tema sistem, atau dukungan fitur browser. Jika pengecekan dilakukan saat render awal, hasilnya bisa berbeda di server dan client.
// Salah: SSR tidak tahu hasil matchMedia pengguna
'use client'
export default function AssistantPanel() {
const compact = window.matchMedia('(max-width: 768px)').matches
return compact ? <MobilePanel /> : <DesktopPanel />
}Gunakan fallback stabil saat render pertama, lalu ubah setelah mount. Atau, jika perbedaan markup terlalu besar, isolasi komponen sebagai client-only.
5. Streaming SSR, Suspense, dan data yang belum sinkron
Pada App Router, server component, streaming, dan suspense membuat aliran render lebih kompleks. Mismatch bisa muncul jika fallback server berbeda dengan state awal client component, atau jika data yang digunakan client belum sama dengan data yang menjadi dasar SSR.
Contoh pola yang rawan:
- Server merender fallback Loading..., tetapi client component langsung merender hasil dari store lokal.
- Server dan client mengambil data dari sumber berbeda pada waktu yang berbeda.
- Komponen client membaca cache lokal sebelum hydration selesai.
Solusinya adalah memastikan fallback dan initial state benar-benar sejalan. Jika server mengirim daftar kosong sebagai state awal, client render pertama juga harus daftar kosong, lalu update setelah effect atau revalidasi.
6. State awal berbeda antara server dan client
Ini akar dari banyak kasus. Misalnya komponen chat AI memiliki state awal yang dihitung dari parameter browser, session lokal, atau eksperimen A/B yang hanya tersedia di client.
// Salah: state awal ditentukan dari browser saat render
'use client'
import { useState } from 'react'
export default function ChatLauncher() {
const [open] = useState(() => window.innerWidth > 1024)
return open ? <aside>AI Chat</aside> : <button>Open Chat</button>
}Gunakan initial state yang netral dan konsisten:
'use client'
import { useEffect, useState } from 'react'
export default function ChatLauncher() {
const [open, setOpen] = useState(false)
useEffect(() => {
if (window.innerWidth > 1024) setOpen(true)
}, [])
return open ? <aside>AI Chat</aside> : <button>Open Chat</button>
}Checklist audit hydration React
Berikut checklist yang bisa dipakai saat mengaudit komponen bermasalah:
- Periksa output render awal
Bandingkan apa yang dirender server dengan apa yang dirender client sebelumuseEffectberjalan. - Cari sumber data non-deterministik
Audit pemakaianDate,Math.random, ID acak, dan nilai yang dihitung ulang saat render. - Audit akses browser-only API
Cariwindow,document,localStorage,sessionStorage,matchMedia, dan API serupa di body komponen. - Periksa formatting locale/timezone
Pastikan locale eksplisit, dan jangan mengandalkan timezone lokal pengguna di SSR tanpa sinkronisasi. - Validasi initial state
State awal di client component harus sama dengan asumsi saat SSR. - Periksa fallback Suspense
Fallback yang dikirim server harus kompatibel dengan render awal client component. - Audit cabang render berbasis environment
Hindari logikaif (typeof window !== 'undefined')yang mengubah markup utama saat render. - Isolasi komponen yang sangat browser-dependent
Jika sebuah widget bergantung penuh pada browser context, pertimbangkan client-only rendering. - Periksa library pihak ketiga
Beberapa package melakukan akses DOM saat import atau render awal. - Reproduksi dengan data tetap
Bekukan input agar Anda tahu mismatch berasal dari kode render, bukan dari data yang berubah-ubah.
Contoh salah vs benar pada UI AI/context-aware
Kasus: greeting berdasarkan waktu lokal pengguna
Masalah ini sering muncul pada dashboard atau asisten AI yang menampilkan sapaan seperti “Selamat pagi”. Jika server berada di timezone berbeda dari pengguna, hasilnya bisa tidak sama.
// Salah: hasil bisa beda antara server dan browser pengguna
export default function Greeting() {
const hour = new Date().getHours()
const label = hour < 12 ? 'Selamat pagi' : 'Selamat siang'
return <h2>{label}</h2>
}Pendekatan yang lebih aman:
- Gunakan sapaan netral saat SSR.
- Atau kirim timezone/locale pengguna dari request jika memang tersedia dan dapat dipercaya.
'use client'
import { useEffect, useState } from 'react'
export default function Greeting() {
const [label, setLabel] = useState('Halo')
useEffect(() => {
const hour = new Date().getHours()
setLabel(hour < 12 ? 'Selamat pagi' : 'Selamat siang')
}, [])
return <h2>{label}</h2>
}Trade-off-nya: ada update setelah mount. Jika Anda ingin menghindari perubahan visual, kirim preferensi waktu dari server atau tampilkan teks yang tidak bergantung waktu.
Kasus: personalisasi dari localStorage
// Salah
'use client'
export default function PersonaBadge() {
const persona = localStorage.getItem('persona') || 'general'
return <span>Mode: {persona}</span>
}// Benar
'use client'
import { useEffect, useState } from 'react'
export default function PersonaBadge() {
const [persona, setPersona] = useState('general')
useEffect(() => {
const value = window.localStorage.getItem('persona')
if (value) setPersona(value)
}, [])
return <span>Mode: {persona}</span>
}Strategi isolasi komponen client-only
Tidak semua komponen layak dipaksa SSR. Untuk widget AI yang sepenuhnya bergantung pada browser context, microphone, ukuran viewport, canvas, atau state lokal, rendering client-only sering lebih aman.
Di Next.js, pendekatan umum adalah memisahkan shell yang stabil dari widget interaktif yang hanya dimuat di client. Tujuannya bukan menghindari SSR secara membabi buta, melainkan membatasi area yang rawan mismatch.
// app/page.tsx
import dynamic from 'next/dynamic'
const ClientOnlyAssistant = dynamic(() => import('./ClientOnlyAssistant'), {
ssr: false,
loading: () => <div>Memuat asisten...</div>,
})
export default function Page() {
return (
<section>
<h2>Asisten</h2>
<ClientOnlyAssistant />
</section>
)
}Kapan memilih client-only?
- Komponen bergantung besar pada API browser.
- Markup awal memang tidak bermakna tanpa context pengguna.
- Biaya menjaga SSR tetap deterministik lebih tinggi daripada manfaat SEO atau time-to-content untuk widget tersebut.
Trade-off:
- Konten komponen tidak tersedia dari SSR.
- Interaktivitas baru muncul setelah JavaScript dimuat.
- Jika dipakai terlalu banyak, manfaat SSR berkurang.
Pola yang sering efektif adalah SSR untuk shell halaman, client-only untuk widget AI yang sangat dinamis.
Logging mismatch dan observabilitas sederhana
Hydration mismatch sering terasa “acak” karena hanya muncul pada kondisi tertentu: locale tertentu, ukuran layar tertentu, atau user dengan state lokal tertentu. Karena itu, logging sangat membantu.
1. Log input render yang memengaruhi markup
Tambahkan logging pada nilai yang menentukan cabang render, misalnya locale, timezone, tema, persona, atau hasil feature detection. Fokus pada nilai yang dipakai sebelum render pertama.
'use client'
import { useEffect } from 'react'
export default function DebugClientContext() {
useEffect(() => {
console.debug('hydration-context', {
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
theme: window.localStorage.getItem('theme'),
width: window.innerWidth,
})
}, [])
return null
}Jangan biarkan log debug seperti ini aktif permanen di production tanpa kontrol, terutama jika ada data sensitif.
2. Tambahkan penanda server vs client
Untuk komponen yang mencurigakan, tampilkan atribut data atau komentar sementara agar Anda tahu HTML awal berasal dari jalur render mana. Ini berguna saat memeriksa DOM hasil SSR dan setelah hydration.
3. Gunakan Error Boundary dan monitoring frontend
Peringatan hydration kadang muncul di console tanpa mudah dikorelasikan ke komponen penyebab. Jika Anda memakai alat monitoring frontend, kelompokkan error dan warning berdasarkan route, user agent, locale, dan ukuran viewport agar pola lebih mudah terlihat.
Catatan: hydration mismatch tidak selalu melempar exception fatal. Sering kali hanya muncul sebagai warning, tetapi dampaknya tetap nyata: event handler gagal menempel sesuai harapan, subtree dirender ulang, atau UI berkedip.
Langkah debugging bertahap di Next.js App Router
Berikut alur debugging yang praktis untuk App Router:
1. Reproduksi pada halaman dan route tertentu
Pastikan Anda tahu route mana yang bermasalah, apakah hanya pada hard refresh, apakah hanya di production build, dan apakah hanya pada user dengan kondisi tertentu. Mismatch sering tidak terlihat di development dengan data yang berubah cepat.
2. Uji production build secara lokal
Beberapa masalah baru terlihat pada mode production. Jalankan build lalu start aplikasi seperti deployment biasa.
npm run build
npm run startJika Anda memakai package manager lain, gunakan padanan perintah yang setara.
3. Sederhanakan subtree yang dicurigai
Komentari komponen secara bertahap sampai warning hilang. Setelah area masalah sempit, audit:
- prop yang datang dari server component,
- state awal di client component,
- semua cabang render yang bergantung environment.
4. Pisahkan server concern dan client concern
Pada App Router, sering kali solusi terbaik adalah memindahkan logika yang benar-benar server-safe ke server component, dan logika browser-dependent ke client component. Hindari komponen client yang pada render pertamanya masih mencoba “menebak” context browser lalu membentuk markup berbeda.
5. Bekukan data input
Jika komponen menerima hasil AI, eksperimen, atau personalisasi, gunakan fixture atau mock agar nilainya tetap. Dengan begitu Anda bisa memastikan apakah mismatch berasal dari perbedaan data atau dari logika render.
6. Verifikasi fallback Suspense
Periksa apakah fallback yang dirender server kompatibel dengan state awal client. Jika fallback menampilkan struktur DOM berbeda total dan client segera merender hasil akhir dari store lokal, mismatch lebih mudah terjadi.
7. Audit import pihak ketiga
Jika warning tetap muncul meski komponen Anda tampak aman, lihat package yang dipakai. Library visualisasi, editor, atau widget tertentu kadang mengakses DOM saat import. Solusinya bisa berupa dynamic import client-only atau membungkus inisialisasi di effect.
8. Gunakan suppressHydrationWarning secara sangat terbatas
React menyediakan mekanisme untuk mengabaikan perbedaan tertentu, tetapi ini bukan solusi utama. Gunakan hanya untuk kasus teks atau atribut yang memang Anda terima berbeda dan tidak memengaruhi struktur/interaktivitas. Jika dipakai terlalu cepat, akar masalah akan tertutup.
// Gunakan hanya untuk kasus sempit yang memang disengaja
<span suppressHydrationWarning>{clientOnlyText}</span>Kalau Anda perlu menambah ini di banyak tempat, biasanya ada masalah desain render yang lebih mendasar.
Pola perbaikan yang paling sering berhasil
- Kirim input awal dari server bila nilai tersebut memengaruhi markup awal, misalnya tema dari cookie, locale eksplisit, atau data personalisasi yang sudah diketahui saat request.
- Gunakan placeholder stabil untuk nilai browser-only, lalu update di
useEffect. - Kurangi cabang render pada initial render; tunda keputusan yang bergantung browser sampai setelah mount.
- Isolasi widget browser-dependent menjadi client-only jika SSR tidak memberi nilai tambah berarti.
- Pastikan state awal sama antara yang diasumsikan server dan yang dipakai client component.
- Tentukan locale secara eksplisit dan jangan mengandalkan default environment.
Kesalahan umum saat memperbaiki mismatch
- Menganggap
'use client'otomatis menyelesaikan mismatch. Tidak. Client component tetap bisa di-pre-render dan tetap harus punya initial render yang konsisten. - Menambahkan guard
typeof windowdi body komponen, tetapi tetap menghasilkan markup berbeda. - Memindahkan semua hal ke
useEffecttanpa memikirkan UX, sehingga halaman berkedip atau shell menjadi kosong. - Menonaktifkan SSR terlalu luas, padahal hanya satu widget yang bermasalah.
- Mengabaikan timezone/locale karena bug tidak muncul di mesin developer.
Penutup
Dalam aplikasi modern dengan UI AI yang makin adaptif, audit hydration React bukan pekerjaan opsional. Semakin banyak konteks yang memengaruhi UI, semakin penting memastikan bahwa SSR dan render pertama di browser menggunakan input yang sama. Jika tidak, mismatch akan muncul dalam bentuk warning, flicker, atau perilaku interaktif yang tidak konsisten.
Pendekatan paling efektif biasanya sederhana: stabilkan render awal, pindahkan akses browser API ke useEffect, kirim data awal dari server bila memungkinkan, dan isolasi komponen yang memang sangat client-dependent. Di Next.js App Router, kombinasi server component, client component, suspense, dan streaming memberi fleksibilitas besar, tetapi juga menuntut disiplin dalam menjaga determinisme render.
Jika Anda sedang membangun antarmuka AI atau context-aware UI, anggap SSR sebagai kontrak: HTML awal harus dapat dipertanggungjawabkan, bukan hasil tebakan browser yang belum tersedia.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!