Error Hydration Next.js: Penyebab dan Cara Memperbaikinya

Foto oleh Bernd Dittrich on Unsplash
Artinya HTML yang dirender Next.js di server tidak cocok dengan hasil render pertama React di browser. React melakukan hydrate dengan memasang handler pada HTML server yang ada, jadi jika pohon klien berbeda, ia tidak bisa mempercayai markup itu, memberi peringatan di development, dan jatuh ke render klien yang lebih lambat untuk subtree tersebut.
Penyebab umumnya adalah penyusunan HTML tidak valid yang diperbaiki browser, API khusus browser seperti window atau localStorage saat render, output yang bergantung waktu atau locale seperti konstruktor Date dan format angka, nilai acak seperti Math.random, serta gangguan pihak ketiga dari ekstensi browser atau minifikasi CDN.
Render placeholder stabil di server dan setel nilai sebenarnya di dalam useEffect, yang hanya berjalan di browser, sehingga render server dan render klien pertama cocok. Untuk satu elemen timestamp yang tidak terhindarkan, Anda bisa memakai suppressHydrationWarning, tetapi gunakan seperlunya sebagai escape hatch.
Hanya ketika teks atau atribut satu elemen memang tidak terhindarkan untuk dicocokkan, seperti timestamp. Ia bekerja satu level dalam dan menghentikan React menambal teks yang mismatch, jadi jangan pernah membungkus seluruh subtree dengannya. Untuk komponen yang tidak bisa dirender di server, nonaktifkan SSR dengan next/dynamic dan ssr false.
Ya. Ekstensi bisa menyuntik atau menulis ulang atribut dan markup sebelum React melakukan hydrate, menghasilkan mismatch yang bukan dari kode Anda. Reproduksi halaman di jendela private dengan ekstensi dimatikan; jika error hilang, ekstensi adalah penyebabnya dan komponen Anda baik-baik saja.

Foto oleh Bernd Dittrich on Unsplash
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.
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.
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.
Teks error saja jarang menunjuk baris yang persis, jadi saya menelusuri checklist yang sama setiap kali alih-alih menebak:
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.
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.
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 |
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.