Ringkasan Utama
Error hydration Next.js berarti HTML yang dirender di server tidak cocok dengan hasil render pertama React di sisi klien. Perbaikannya adalah membuat kedua render identik, atau menunda nilai yang benar-benar khusus klien ke dalam useEffect agar output awal tetap sama di kedua sisi.
Sedikit error Next.js yang membingungkan pada pandangan pertama seperti hydration failed because the initial UI does not match what was rendered on the server. Halaman sering terlihat baik-baik saja, tetapi konsol menyala merah dan interaktivitas terasa rusak. Saya sudah sering mengalaminya di berbagai proyek App Router, dan penyebabnya selalu berbentuk masalah yang sama.
Di tulisan ini saya jelaskan apa itu hydration sebenarnya, kenapa mismatch terjadi, cara yang bisa diulang untuk melacak baris penyebabnya, dan perbaikan yang benar untuk tiap penyebab. Tanpa menebak, hanya pola yang benar-benar direkomendasikan oleh dokumentasi React dan Next.js.
Apa arti sebenarnya error hydration
Next.js melakukan prerender halaman menjadi HTML di server. Di browser, React lalu melakukan hydrate terhadap HTML itu, menelusuri pohon komponen yang sama dan memasang event handler pada DOM yang sudah ada alih-alih membuatnya ulang. Hydration hanya berhasil jika pohon yang dibangun React di klien menghasilkan output yang sama seperti di server.
Ketika render klien pertama berbeda dari HTML server, React tidak bisa mempercayai markup tersebut. Ia memberi peringatan di mode development dan jatuh kembali ke render klien untuk subtree itu, yang lebih lambat dan bisa memasang handler ke elemen yang salah. Karena itu dokumentasinya tegas: perlakukan mismatch sebagai bug dan perbaiki, jangan menutupinya.
Penyebab yang umum
Hampir setiap hydration mismatch yang saya debug bermuara pada satu dari sekumpulan kecil penyebab. Dokumentasi React dan Next.js menyebut pelaku yang sama, jadi begitu Anda mengenali bentuknya, biasanya Anda bisa menamai penyebabnya sebelum membuka komponennya.
- Penyusunan HTML yang tidak valid dan diam-diam diperbaiki browser, seperti div di dalam p, atau p di dalam p lain. Server memancarkan markup yang Anda maksud, browser menulisnya ulang, dan kedua pohon tidak lagi sejajar.
- API khusus browser yang dipakai saat render, seperti window, localStorage, atau matchMedia. Semua ini tidak ada di server, sehingga cabang server dan klien berbeda, terutama saat dijaga dengan pemeriksaan typeof window.
- Output yang bergantung waktu dan locale saat render, seperti konstruktor Date, toLocaleTimeString, atau format angka dan mata uang yang berbeda antara timezone atau locale server dan pengunjung.
- Nilai acak atau non-deterministik yang dibuat saat render, seperti Math.random atau id unik baru, yang secara alami menghasilkan hasil berbeda setiap kali sehingga berbeda antara server dan klien.
- Hal-hal di luar kode Anda sepenuhnya, yang paling umum adalah ekstensi browser yang menyuntik atau menulis ulang atribut dan markup sebelum React melakukan hydrate, ditambah minifikasi CDN atau setup CSS-in-JS yang salah konfigurasi.
Cara sistematis untuk menemukannya
Teks error saja jarang menunjuk baris yang persis, jadi saya menelusuri checklist yang sama setiap kali alih-alih menebak:
- Baca diff yang dicetak React. Next.js modern menampilkan teks atau atribut yang mismatch; catat apakah itu konten teks, sebuah atribut, atau seluruh elemen yang muncul hanya di satu sisi.
- Reproduksi secara bersih. Buka halaman di jendela private dengan ekstensi dimatikan. Jika error hilang, sebuah ekstensi sedang menulis ulang HTML Anda dan kode Anda baik-baik saja.
- Bisect pohonnya. Komentari atau hubung-singkatkan separuh komponen sampai peringatan berhenti, yang mengisolasi subtree penyebab dengan cepat tanpa membaca setiap baris.
- Tanyakan apa yang bisa berbeda antara server dan klien untuk node itu: jam, locale, nilai acak, pemeriksaan window, atau tag bersarang yang akan diperbaiki browser. Jawaban itu menamai perbaikannya.
Perbaikan yang benar: jaga kedua render tetap identik
Perbaikan utama bukanlah membungkam peringatan, melainkan membuat render server dan render klien pertama menghasilkan output yang sama. Untuk nilai yang benar-benar khusus klien, render placeholder yang stabil di server dan isi nilai sebenarnya setelah mount di dalam useEffect, yang hanya berjalan di browser.
// BEFORE — hydration mismatch.
// The server renders its own clock time; the browser renders a
// different time at hydration, so the initial UIs never match.
export default function LastUpdated() {
return <p>Last updated: {new Date().toLocaleTimeString()}</p>;
}
// AFTER — stable on the server, filled in on the client.
// The server and first client render both output an empty string
// (they match), then useEffect swaps in the real time after mount.
"use client";
import { useState, useEffect } from "react";
export default function LastUpdated() {
const [time, setTime] = useState("");
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>Last updated: {time}</p>;
}Ini adalah pola yang direkomendasikan dokumentasi Next.js untuk nilai yang bergantung waktu dan khusus browser. Server dan render klien pertama sama-sama memancarkan placeholder kosong yang sama, jadi hydration berhasil, lalu effect memperbarui DOM sesaat kemudian tanpa mismatch apa pun.
Gunakan next/dynamic dengan ssr false ketika sebuah komponen memang tidak pernah bisa dirender secara bermakna di server, seperti chart yang bergantung pada ukuran window. Ini menonaktifkan prerender untuk komponen itu saja, jadi tidak ada yang dipancarkan di server untuk di-mismatch.
Kapan suppressHydrationWarning adalah alat yang tepat
Kadang sebuah perbedaan memang tidak terhindarkan untuk satu elemen, contoh klasiknya adalah timestamp. Untuk kasus sempit itu React menyediakan suppressHydrationWarning. Jika sebuah komponen sama sekali tidak boleh dirender di server, menonaktifkan SSR dengan dynamic import adalah pilihan yang lebih bersih.
// Escape hatch: only for a single element whose value is
// genuinely unavoidable to match, such as a timestamp.
// It patches one level deep only — do not wrap whole subtrees.
<time dateTime="2026-08-01" suppressHydrationWarning>
{new Date().toLocaleDateString()}
</time>
// For content that must not render on the server at all,
// disable prerendering for that component instead:
import dynamic from "next/dynamic";
const ClientOnlyChart = dynamic(() => import("./chart"), {
ssr: false,
});Perlakukan suppressHydrationWarning sebagai escape hatch, bukan default. Ia hanya bekerja satu level dalam, dan saat disetel React tidak akan mencoba menambal konten teks yang mismatch, jadi apa pun di bawah elemen itu berjuang sendiri.
Jangan membungkus seluruh subtree dengan suppressHydrationWarning hanya agar konsol merah hilang. Ia menyembunyikan bug nyata satu level di bawahnya, dan karena React berhenti menambal teks, Anda bisa mengirim UI yang diam-diam berbeda antara server dan klien tanpa peringatan apa pun.
Penyebab dan perbaikan sekilas
Berikut pemetaan yang selalu saya ingat, mencocokkan tiap penyebab umum dengan perbaikan yang didukung dokumentasi:
| Penyebab | Kenapa mismatch | Perbaikan yang benar |
|---|---|---|
| Penyusunan HTML tidak valid | Browser memperbaiki markup, sehingga DOM tidak lagi cocok dengan pohon server | Perbaiki penyusunan agar elemen blok tidak diletakkan di dalam p atau a di dalam a |
| API khusus browser saat render | window atau localStorage undefined di server, sehingga cabangnya berbeda | Pindahkan akses ke dalam useEffect dan render placeholder stabil lebih dulu |
| Format Date, locale, atau angka | Timezone atau locale server berbeda dari pengunjung, sehingga teksnya berbeda | Format di dalam useEffect, atau pakai suppressHydrationWarning pada satu timestamp |
| Nilai acak atau unik | Math.random dan id baru menghasilkan nilai berbeda tiap render | Buat nilainya di useEffect, atau pakai id stabil dari useId |
| Ekstensi browser atau CDN | Kode pihak ketiga menulis ulang HTML sebelum React melakukan hydrate | Pastikan di profil bersih; matikan auto-minify CDN; itu bukan kode Anda |
Penutup
Error hydration terasa misterius sampai Anda menghayati satu aturan di baliknya: render server dan render klien pertama harus menghasilkan output yang identik. Begitu itu klik, setiap perbaikan mengikuti dari menanyakan apa yang bisa berbeda untuk sebuah node, lalu memakai useEffect, dynamic import, atau suppressHydrationWarning yang tepat sasaran dalam urutan itu.

