Hydration mismatch adalah salah satu error yang paling membingungkan saat membangun aplikasi dengan Next.js. Gejalanya sering terlihat sederhana: ada peringatan bahwa konten yang dirender di server tidak cocok dengan hasil render di client. Namun akar masalahnya hampir selalu sama: output HTML awal dari server berbeda dengan output render pertama di browser.
Dalam Next.js 16, masalah ini tetap relevan karena model rendering modern tetap mengandalkan kombinasi SSR, React Server Components, dan Client Components. Artinya, kita perlu disiplin memastikan bahwa bagian UI yang dirender di server menghasilkan markup yang stabil, deterministik, dan tidak bergantung pada API browser saat fase render.
Artikel ini adalah panduan praktis untuk memahami, mendiagnosis, dan memperbaiki hydration mismatch dari akar masalahnya. Fokusnya bukan sekadar menghilangkan warning, tetapi memastikan arsitektur render Anda konsisten dan mudah dipelihara.
Memahami apa itu hydration mismatch
Saat halaman dirender di server, Next.js mengirim HTML awal ke browser. Setelah JavaScript client dimuat, React melakukan proses hydration: menghubungkan event handler dan state ke HTML yang sudah ada. Agar proses ini berhasil, React mengharapkan struktur dan isi DOM awal di browser sama dengan yang dihasilkan di server.
Hydration mismatch terjadi ketika asumsi itu gagal. Misalnya:
- Server merender teks
Selamat pagi, tetapi client saat pertama render menghasilkanSelamat malam. - Server merender elemen kosong, tetapi client langsung merender data dari
localStorage. - Server menghasilkan daftar dengan urutan tertentu, tetapi client menghitung ulang dan menghasilkan urutan berbeda.
React lalu memberi warning atau bahkan mengganti subtree tertentu. Dalam kasus ringan, UI tampak normal tetapi ada warning di console. Dalam kasus berat, event handler bisa menempel ke node yang salah, tampilan berkedip, atau state awal menjadi tidak konsisten.
Aturan praktis: jika sebuah nilai bisa berbeda antara server dan browser pada render pertama, jangan gunakan nilai itu langsung untuk menghasilkan markup SSR.
Penyebab umum hydration mismatch di Next.js
1. Nilai non-deterministik saat render: tanggal, waktu, angka acak
Sumber mismatch yang paling sering adalah nilai yang berubah setiap render, seperti new Date(), Date.now(), atau Math.random(). Jika dipanggil saat render, server dan client hampir pasti menghasilkan output berbeda.
// Buruk: hasil bisa berbeda antara server dan client
export default function Header() {
return <p>Dimuat pada: {new Date().toLocaleTimeString()}</p>
}Pada server, waktu dirender berdasarkan jam server. Di browser, render pertama bisa terjadi beberapa milidetik atau detik kemudian, bahkan dengan locale berbeda. Hasilnya mismatch.
Perbaikan: render placeholder yang stabil terlebih dahulu, lalu isi nilainya di client menggunakan useEffect.
'use client'
import { useEffect, useState } from 'react'
export default function Header() {
const [time, setTime] = useState('')
useEffect(() => {
setTime(new Date().toLocaleTimeString())
}, [])
return <p>Dimuat pada: {time || '...'}</p>
}Mengapa ini bekerja? Karena render awal di client sama dengan HTML server: keduanya menampilkan .... Setelah hydration selesai, useEffect berjalan hanya di browser dan memperbarui state secara aman.
2. Mengakses window, document, atau localStorage saat render
API browser tidak tersedia di server. Kadang developer mencoba menghindari error dengan kondisi seperti typeof window !== 'undefined', tetapi tetap menghitung hasil render berbeda antara server dan client.
// Buruk: markup awal bisa berbeda
'use client'
export default function ThemeLabel() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme')
: 'light'
return <span>Tema: {theme}</span>
}Server akan merender Tema: light, sementara client bisa langsung merender Tema: dark dari localStorage. React melihat perbedaan ini saat hydration.
Perbaikan: ambil nilai browser setelah mount.
'use client'
import { useEffect, useState } from 'react'
export default function ThemeLabel() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const saved = localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return <span>Tema: {theme}</span>
}Jika Anda ingin menghindari tampilan tema yang salah sesaat, pertimbangkan strategi yang lebih kuat seperti menyimpan preferensi tema di cookie dan membacanya di server, sehingga SSR dan client memiliki sumber data awal yang sama.
3. Hasil render berbeda antara server dan client
Mismatch tidak selalu berasal dari API browser. Kadang logika bisnisnya sendiri menghasilkan output berbeda. Beberapa contohnya:
- Mengurutkan data dengan comparator yang tidak stabil.
- Mengandalkan locale default yang berbeda antara environment server dan browser.
- Menggunakan data yang berubah di antara request dan hydration.
- Menghasilkan ID atau key secara acak saat render.
Contoh yang sering luput adalah format tanggal berbasis locale:
// Potensial mismatch jika locale/timezone berbeda
export default function PublishedAt({ publishedAt }) {
return <p>{new Date(publishedAt).toLocaleString()}</p>
}Jika server berjalan dengan timezone UTC dan browser pengguna di WIB, output string bisa berbeda. Solusinya adalah menentukan format yang konsisten, misalnya memformat di server dengan locale/timezone eksplisit, atau menunda format lokal ke client setelah hydration.
export default function PublishedAt({ publishedAt }) {
const formatted = new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Jakarta',
}).format(new Date(publishedAt))
return <p>{formatted}</p>
}Pendekatan ini aman jika Anda memang ingin output SSR tetap identik untuk semua pengguna. Jika ingin mengikuti timezone browser pengguna, formatlah di client setelah mount.
4. Library pihak ketiga yang tidak SSR-friendly
Banyak library UI, chart, editor, atau peta mengakses DOM saat modul di-import atau saat render pertama. Di aplikasi berbasis SSR, library seperti ini bisa menyebabkan error runtime atau hydration mismatch.
Gejalanya biasanya:
- Error seperti
window is not defined. - Komponen merender placeholder berbeda di server dan client.
- Markup internal library berubah setelah mount.
Solusi paling umum adalah memuat komponen tersebut hanya di client dengan dynamic import dan menonaktifkan SSR untuk komponen itu.
import dynamic from 'next/dynamic'
const ClientOnlyChart = dynamic(() => import('./Chart'), {
ssr: false,
loading: () => <p>Memuat chart...</p>,
})
export default function Dashboard() {
return <ClientOnlyChart />
}Mengapa ini bekerja? Karena komponen chart tidak dirender di server, sehingga tidak ada HTML SSR yang harus dicocokkan saat hydration. Trade-off-nya, konten chart tidak tersedia di HTML awal dan baru muncul setelah JavaScript client dimuat.
Contoh bug kecil dan perbaikannya
Bug: membaca lebar layar saat render
'use client'
export default function ScreenInfo() {
const isMobile = window.innerWidth < 768
return <p>Mode: {isMobile ? 'Mobile' : 'Desktop'}</p>
}Masalahnya ada dua. Pertama, di server tidak ada window. Kedua, sekalipun dibungkus pengecekan, output awal server dan client bisa berbeda.
Perbaikan 1: gunakan useEffect
'use client'
import { useEffect, useState } from 'react'
export default function ScreenInfo() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const update = () => setIsMobile(window.innerWidth < 768)
update()
window.addEventListener('resize', update)
return () => window.removeEventListener('resize', update)
}, [])
return <p>Mode: {isMobile ? 'Mobile' : 'Desktop'}</p>
}Ini cocok jika perbedaan tampilan kecil dan aman muncul setelah mount.
Perbaikan 2: pisahkan komponen client
Jika logika benar-benar bergantung pada browser, jangan paksa komponen tersebut menjadi bagian penting dari SSR. Pisahkan dengan jelas.
// app/page.tsx
import ScreenInfoClient from './ScreenInfoClient'
export default function Page() {
return (
<main>
<h2>Dashboard</h2>
<ScreenInfoClient />
</main>
)
}// app/ScreenInfoClient.tsx
'use client'
import { useEffect, useState } from 'react'
export default function ScreenInfoClient() {
const [label, setLabel] = useState('Mendeteksi...')
useEffect(() => {
setLabel(window.innerWidth < 768 ? 'Mobile' : 'Desktop')
}, [])
return <p>Mode: {label}</p>
}Pemisahan ini membuat batas antara rendering server dan browser lebih eksplisit, sehingga lebih mudah dirawat dan didiagnosis.
Langkah diagnosis yang sistematis
Salah satu kesalahan umum saat debugging hydration mismatch adalah langsung menebak-nebak. Pendekatan yang lebih efektif adalah mempersempit sumber mismatch secara sistematis.
1. Baca pesan error dan identifikasi subtree yang bermasalah
Lihat console browser dan terminal development. Biasanya React atau Next.js memberi petunjuk elemen mana yang tidak cocok. Fokus dulu pada komponen yang disebut, bukan seluruh halaman.
2. Cari nilai dinamis yang dievaluasi saat render
Audit komponen tersebut dan cari pola berikut:
new Date(),Date.now(),Math.random()window,document,navigator,localStorage,sessionStorage- format locale tanpa timezone/locale eksplisit
- perhitungan yang bergantung pada ukuran viewport
- data hasil fetch yang berubah antara server dan client
Jika ada salah satu dari pola di atas di dalam render function, itu kandidat utama.
3. Bandingkan output server dan render awal client
Untuk komponen kecil, tambahkan logging sementara terhadap nilai yang dipakai untuk merender UI. Tujuannya bukan log setelah useEffect, tetapi log saat render pertama.
'use client'
export default function DebugComponent() {
const value = typeof window === 'undefined' ? 'server' : 'client'
console.log('render value:', value)
return <div>{value}</div>
}Jika nilai render berbeda, Anda sudah menemukan akar masalah: output awal tidak deterministik.
4. Isolasi library pihak ketiga
Jika mismatch muncul setelah menambahkan editor, chart, date picker, map, carousel, atau animation library, coba lepaskan sementara. Jika warning hilang, bungkus komponen tersebut dengan dynamic(..., { ssr: false }) atau cari dokumentasi SSR dari library tersebut.
5. Uji dengan data statis
Ganti data API atau state turunan dengan data hardcoded sementara. Jika mismatch hilang, masalahnya ada di sumber data atau transformasinya, bukan pada markup dasar komponen.
6. Periksa key dan struktur list
List dengan key yang tidak stabil bisa memperparah mismatch. Hindari Math.random() atau index array jika urutan item bisa berubah. Gunakan ID yang benar-benar stabil dari data.
Kapan menggunakan useEffect, dynamic import, atau pemisahan client component?
Gunakan useEffect jika:
- Nilai hanya tersedia di browser.
- Anda bisa menerima placeholder atau state awal netral saat SSR.
- Perubahan UI setelah mount tidak merusak pengalaman pengguna.
Gunakan dynamic import dengan ssr: false jika:
- Library pihak ketiga tidak aman untuk SSR.
- Komponen sangat bergantung pada DOM atau browser API.
- Anda rela mengorbankan SSR untuk komponen tertentu.
Pisahkan Client Component jika:
- Bagian halaman memang interaktif dan bergantung pada state browser.
- Anda ingin menjaga bagian server tetap stabil dan minimal.
- Anda ingin batas tanggung jawab antara SSR dan client lebih jelas.
Penting untuk dipahami bahwa ketiga pendekatan ini bukan saling menggantikan. Sering kali solusi terbaik adalah kombinasi: halaman utama tetap SSR, widget interaktif dipisah sebagai Client Component, dan library yang bermasalah dimuat dengan dynamic import.
Kesalahan umum yang sering terulang
- Menganggap
'use client'otomatis menyelesaikan mismatch. Tidak. Client Component tetap bisa diprerender dan tetap harus menghasilkan output awal yang konsisten. - Membungkus akses browser dengan
typeof windowlangsung di render. Ini menghindari crash, tetapi tidak selalu menghindari mismatch. - Menonaktifkan SSR terlalu cepat. Ini memang praktis, tetapi bisa merugikan SEO, performa HTML awal, dan pengalaman loading.
- Menggunakan nilai acak untuk key atau ID. Ini membuat DOM sulit dicocokkan antara server dan client.
- Mengabaikan locale dan timezone. Formatting yang terlihat sepele bisa menghasilkan string berbeda.
Penutup
Hydration mismatch di Next.js 16 hampir selalu berakar pada satu hal: server dan client tidak sepakat tentang output render awal. Karena itu, solusi yang benar bukan sekadar membungkam warning, tetapi memastikan fase render pertama bersifat deterministik.
Mulailah dengan pertanyaan sederhana: apakah komponen ini menggunakan nilai yang hanya tersedia di browser, berubah terhadap waktu, atau dihasilkan secara acak? Jika ya, pindahkan logikanya ke useEffect, pisahkan menjadi Client Component, atau nonaktifkan SSR hanya untuk bagian yang memang tidak SSR-friendly.
Dengan pola diagnosis yang sistematis dan pemisahan tanggung jawab yang jelas, hydration mismatch bukan lagi error yang misterius. Ia menjadi sinyal bahwa batas antara server rendering dan browser rendering belum dikelola dengan tepat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!