Hydration mismatch pada SSR editor HTML biasanya terjadi karena markup yang dihasilkan server tidak identik dengan yang dihitung ulang di browser. Pada kasus konten dari Markdown, WYSIWYG, atau HTML siap-paste yang kompleks, selisih kecil seperti atribut yang berubah, whitespace, ID acak, atau hasil sanitasi yang berbeda sudah cukup untuk memicu warning dan perilaku UI yang tidak konsisten.
Jika Anda memakai Next.js untuk merender rich text, prinsip dasarnya sederhana: hasilkan HTML yang deterministik, stabil, dan sama persis di server maupun client. Dalam praktiknya, ini berarti menghindari transformasi ganda, menyamakan pipeline sanitasi, membatasi kode yang bergantung pada browser saat render awal, dan memindahkan editor interaktif ke client-only bila memang tidak bisa dibuat stabil.
Ini relevan untuk kasus output HTML siap-paste yang kompleks, seperti konten desain atau dokumen yang dihasilkan dari tooling internal maupun repo seperti isjiamu/gzh-design-skill. Fokus artikel ini bukan pada repo tersebut, melainkan pada praktik engineering agar HTML hasil export tetap aman dirender di SSR tanpa hydration mismatch.
Apa itu hydration mismatch dan kenapa sering terjadi pada rich text
Pada aplikasi SSR, server mengirim HTML awal ke browser. Setelah itu, React melakukan hydration, yaitu memasang event listener dan mencocokkan tree virtual React dengan DOM yang sudah ada. Jika struktur atau isi DOM berbeda dari yang React harapkan, muncul warning seperti:
Warning: Text content did not match. Server: "..." Client: "..."Untuk komponen biasa, mismatch sering berasal dari penggunaan Date.now(), Math.random(), atau akses window saat render. Untuk rich text, sumbernya lebih banyak karena ada pipeline tambahan:
- Markdown atau HTML diparse
- Konten disanitasi
- Plugin editor menambah atribut
- String HTML diubah jadi DOM
- Browser bisa menormalkan markup tertentu
Akibatnya, dua output yang secara visual terlihat sama belum tentu identik sebagai string HTML atau struktur DOM.
Penyebab umum hydration mismatch pada SSR Editor HTML
1. Sanitasi berbeda antara server dan client
Ini salah satu penyebab paling sering. Misalnya server menghapus atribut style atau target, tetapi client mempertahankannya. Atau sebaliknya, urutan atribut dan elemen hasil sanitasi berubah karena library yang dipakai berbeda konteks eksekusinya.
Masalah ini sering muncul jika:
- Sanitasi dilakukan dua kali dengan konfigurasi berbeda
- Server memakai satu library, client memakai library lain
- Ada fallback perilaku berbeda saat API DOM browser tidak tersedia di server
Prinsip aman: sanitasi idealnya dilakukan sekali di server atau di tahap ingest/pre-processing, lalu client hanya merender hasil akhirnya tanpa memodifikasi ulang.
2. ID acak atau atribut yang tidak deterministik
Beberapa plugin editor, syntax highlighter, atau komponen pembungkus konten menambahkan ID seperti heading-123 atau node-8f3a. Jika nilainya berasal dari Math.random(), timestamp, atau urutan yang berubah, server dan client akan menghasilkan markup berbeda.
Contoh buruk:
const id = `section-${Math.random().toString(36).slice(2)}`;Untuk SSR, ID harus dihasilkan secara stabil dari input, misalnya dari slug heading atau hash konten.
3. Waktu render: tanggal, locale, timezone
Kalau HTML rich text menampilkan waktu render, "dibaca 2 menit lalu", atau format tanggal lokal, hasilnya bisa berbeda antara environment server dan browser. Ini terutama sering terjadi jika formatting dilakukan langsung saat render.
Contoh masalah:
<p>Diperbarui: {new Date().toLocaleString()}</p>Server bisa memakai locale atau timezone berbeda dari client. Hasil string-nya pun berubah.
4. Parser DOM menormalkan HTML secara berbeda
Browser tidak selalu mempertahankan string HTML mentah seperti input Anda. Beberapa tag bisa diperbaiki otomatis, dipindah, atau disusun ulang oleh parser HTML. Misalnya nesting yang tidak valid, elemen tabel yang tidak lengkap, atau tag blok di dalam elemen inline.
Jika server menghasilkan HTML yang secara sintaks tampak benar tetapi secara struktur tidak valid, browser dapat mengoreksinya sebelum React melakukan hydration. Hasil akhirnya: DOM nyata berbeda dari ekspektasi React.
5. Penggunaan dangerouslySetInnerHTML dengan transformasi tambahan di client
dangerouslySetInnerHTML sendiri bukan masalah. Yang berbahaya adalah ketika HTML yang disuntik di server tidak sama dengan HTML yang dihitung ulang di client. Ini sering terjadi jika Anda melakukan:
- Sanitasi ulang di browser
- Replace string di
useEffect - Enhancement plugin setelah hydration
- Rendering ulang dari source Markdown yang berbeda
6. Perbedaan whitespace dan newline
Pada konten biasa, whitespace sering diabaikan. Tetapi pada rich text tertentu, terutama dalam pre, code, inline formatting, atau hasil template string, perbedaan newline dan spasi bisa mengubah text node.
Contohnya:
- Server melakukan trim, client tidak
- Markdown parser mengonversi line ending berbeda
- HTML hasil copy-paste mengandung karakter tak terlihat
7. Plugin editor atau enhancer DOM yang berjalan hanya di browser
Beberapa editor dan plugin tidak dirancang untuk SSR. Mereka membaca DOM aktual, menambah wrapper, memindahkan node, atau menyisipkan toolbar setelah mount. Jika plugin seperti ini dipaksa ikut dalam render awal SSR, mismatch hampir pasti terjadi.
Contoh pola yang rawan:
- Syntax highlighting yang memodifikasi HTML setelah mount
- Plugin anchor heading yang menambah ikon/link di browser saja
- Editor read-only yang tetap menginisialisasi state internal berbasis DOM
Contoh problem nyata di Next.js
Misalkan Anda menerima HTML dari CMS atau hasil konversi Markdown, lalu merendernya pada halaman SSR.
export default function Article({ html }) {
const safeHtml = sanitizeHtml(html)
return (
<article dangerouslySetInnerHTML={{ __html: safeHtml }} />
)
}Sekilas aman. Tetapi jika di client komponen yang sama menghitung ulang sanitizeHtml(html) dengan konfigurasi atau environment berbeda, hasil akhir bisa berubah. Misalnya server menghapus atribut tertentu, sedangkan client mempertahankannya karena parser DOM tersedia.
Contoh lain:
export default function Article({ html }) {
const withAnchors = addHeadingAnchors(html)
return <article dangerouslySetInnerHTML={{ __html: withAnchors }} />
}Kalau addHeadingAnchors membuat ID heading berbasis urutan traversal DOM yang tidak konsisten, hasilnya juga bisa berbeda antara server dan client.
Pada Next.js, masalah ini biasanya terlihat sebagai warning di development. Tetapi jangan anggap warning itu sepele. Di production, mismatch bisa menyebabkan:
- Event handler tidak terpasang pada node yang diharapkan
- UI interaktif terlihat benar tetapi state tidak sinkron
- React membuang subtree dan merender ulang di client
- Biaya render bertambah dan manfaat SSR berkurang
Strategi utama: buat output HTML stabil dan deterministik
1. Satu sumber kebenaran untuk HTML final
Pola yang paling aman adalah: source masuk sekali, diparse dan disanitasi sekali, lalu hasil HTML final disimpan atau dikirim apa adanya ke komponen tampilan.
Alur yang disarankan:
- Terima Markdown atau HTML mentah
- Lakukan transformasi dan sanitasi di server atau pipeline backend
- Simpan HTML final yang sudah stabil
- Komponen Next.js hanya merender string final tersebut
Dengan pola ini, client tidak perlu menghitung ulang transformasi yang berpotensi berbeda.
2. Hindari sanitasi ulang saat hydration
Kalau HTML final sudah aman, jangan sanitasi ulang di komponen React yang ikut dirender di server dan browser. Lebih baik sanitasi dilakukan di satu tempat terpusat dengan konfigurasi tetap.
// Server-side pipeline atau data layer
function prepareRichText(rawHtml) {
const sanitized = sanitize(rawHtml)
return normalizeHtml(sanitized)
}
// Komponen presentasi
export function RichTextView({ html }) {
return <div className="prose" dangerouslySetInnerHTML={{ __html: html }} />
}Kenapa ini bekerja: React hanya menerima string HTML final yang sama di dua sisi, bukan menghitung ulang pipeline yang bisa berbeda.
3. Normalisasi output sebelum dirender
Untuk konten hasil copy-paste atau export yang kompleks, normalisasi sering diperlukan agar output lebih stabil. Misalnya:
- Trim whitespace yang tidak bermakna
- Merapikan nesting tag yang tidak valid
- Menghapus atribut dinamis seperti
data-*tertentu - Menyeragamkan line ending
- Menghapus elemen kosong yang tidak dibutuhkan
Normalisasi ini sebaiknya dilakukan sebelum HTML masuk ke komponen UI.
4. Gunakan ID yang stabil
Jika Anda perlu menambahkan anchor ke heading, buat ID dari teks heading, bukan dari angka acak.
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
}
Jika ada kemungkinan duplikasi heading, gunakan strategi deterministik seperti suffix berbasis urutan kemunculan dalam dokumen yang dihitung sekali pada pipeline server, lalu simpan hasilnya.
5. Jangan render nilai berbasis waktu saat SSR kecuali sudah dibekukan
Jika artikel menampilkan tanggal atau metadata waktu, kirim string final dari server, misalnya ISO string atau format yang sudah jadi. Hindari memanggil formatter waktu saat render awal di browser.
export default function Meta({ updatedAtLabel }) {
return <p>Diperbarui: {updatedAtLabel}</p>
}Kalau butuh versi relatif seperti "3 menit lalu", tampilkan setelah mount di komponen kecil terpisah, bukan di subtree HTML utama yang ingin stabil.
6. Pisahkan viewer HTML dari editor interaktif
Untuk halaman artikel, dokumentasi, atau preview read-only, gunakan renderer HTML yang sederhana. Jangan memaksa editor WYSIWYG lengkap ikut SSR jika kebutuhan Anda hanya menampilkan konten.
Pola yang sehat:
- Viewer SSR: HTML final, aman, stabil, SEO-friendly
- Editor client-only: toolbar, selection state, plugin DOM, undo stack
Pola komponen yang aman di Next.js
Opsi A: SSR penuh untuk viewer yang stabil
Ini pilihan terbaik jika HTML final bisa ditentukan di server dan tidak butuh manipulasi DOM saat mount.
export function RichTextHTML({ html }) {
return (
<section
className="rich-text"
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}Gunakan ini untuk halaman publik yang mengandalkan SEO dan waktu muat awal yang baik.
Opsi B: Client-only untuk editor atau enhancer yang tidak SSR-safe
Jika library editor bergantung pada window, selection API, atau memodifikasi DOM saat mount, lebih aman dimuat hanya di client.
import dynamic from 'next/dynamic'
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
ssr: false,
})
export default function Page() {
return <RichTextEditor />
}Trade-off: ini menghilangkan risiko hydration mismatch dari editor, tetapi bagian tersebut tidak ikut SSR. Untuk halaman edit/admin, ini biasanya masuk akal. Untuk halaman publik SEO, jangan jadikan seluruh konten artikel client-only jika tidak perlu.
Opsi C: SSR viewer, client enhancement terisolasi
Pendekatan kompromi yang sering paling efektif adalah merender HTML utama secara SSR, lalu enhancement non-kritis dijalankan setelah mount pada area terisolasi.
Contohnya:
- SSR untuk isi artikel
- Client-only untuk tombol copy code block
- Client-only untuk TOC interaktif
- Client-only untuk editor preview
Dengan begitu, subtree HTML utama tetap stabil dan searchable.
Checklist diagnosis saat terjadi hydration mismatch
Saat warning muncul, jangan langsung menyalahkan dangerouslySetInnerHTML. Gunakan checklist berikut.
Periksa apakah input server dan client benar-benar sama
- Apakah props
htmlidentik? - Apakah ada fetch ulang di client yang menghasilkan konten berbeda?
- Apakah source Markdown diparse dua kali?
Bandingkan HTML final, bukan source mentah
- Log string HTML final di server
- Log string HTML final sebelum render di client
- Bandingkan perbedaan whitespace, atribut, dan urutan node
Audit sumber nondeterministik
Date.now()new Date()Math.random()- ID dari plugin
- Locale/timezone formatter
Cek validitas struktur HTML
- Apakah ada nesting invalid?
- Apakah tabel, list, atau paragraph ditutup dengan benar?
- Apakah browser mungkin menormalkan markup saat parsing?
Isolasi plugin satu per satu
Jika Anda memakai pipeline parser/editor yang kompleks, nonaktifkan plugin tambahan sementara. Mulai dari output paling sederhana, lalu aktifkan kembali satu per satu sampai mismatch muncul.
Bedakan error development vs production build
Beberapa kasus terasa "hilang" di development tetapi muncul di production, atau sebaliknya. Karena itu verifikasi harus dilakukan di kedua mode.
Langkah verifikasi di development dan production build
1. Verifikasi di mode development
npm run devLalu:
- Buka halaman yang merender rich text
- Perhatikan warning hydration di terminal dan console browser
- Inspect elemen hasil render
- Bandingkan output server dan perubahan DOM setelah mount
Tips praktis:
- Tambahkan logging sementara pada pipeline transformasi
- Simpan snapshot HTML final ke file untuk dibandingkan
- Uji dengan konten yang kompleks: tabel, code block, inline style, nested list
2. Verifikasi di production build
npm run build
npm run startSetelah itu ulangi pengujian pada konten yang sama. Production build penting karena optimasi bundler, urutan eksekusi, dan perilaku rendering bisa berbeda dari dev.
3. Buat kasus uji yang representatif
Jangan hanya menguji paragraf biasa. Untuk konteks HTML siap-paste yang kompleks, siapkan fixture seperti:
- Heading dengan anchor
- Tabel dengan colspan/thead/tbody
- Code block multi-line
- List bertingkat
- Inline style dan atribut class
- Karakter spesial, emoji, dan entity HTML
Contoh strategi implementasi yang lebih stabil
Berikut pola sederhana: siapkan HTML final di layer server, normalisasi, lalu render apa adanya di komponen.
// contoh util server-side
export function prepareHtmlForSSR(rawHtml) {
const sanitized = sanitize(rawHtml)
const normalized = sanitized
.replace(/\r\n/g, '\n')
.replace(/>\s+</g, '><')
.trim()
return normalized
}
// komponen viewer
export function RichTextContent({ html }) {
return <div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: html }} />
}
Jika ada enhancement browser-only, pisahkan:
import dynamic from 'next/dynamic'
const ClientEnhancer = dynamic(() => import('./ClientEnhancer'), { ssr: false })
export default function ArticlePage({ html }) {
return (
<>
<RichTextContent html={html} />
<ClientEnhancer />
</>
)
}Dengan pola ini, subtree artikel tetap deterministik untuk hydration, sementara enhancement interaktif tidak mengganggu SSR utama.
Kapan memakai client-only, dan apa trade-off SEO/performa-nya
Pilih SSR jika:
- Konten harus terindeks mesin pencari
- Halaman publik mengandalkan first render yang cepat
- HTML final bisa dibuat stabil di server
- Viewer bersifat read-only
Pilih client-only jika:
- Library editor memang tidak SSR-safe
- Komponen sangat bergantung pada DOM browser
- Halaman adalah area admin/editor, bukan landing publik
- Biaya menstabilkan SSR lebih besar dari manfaatnya
Trade-off yang perlu dipahami
- SEO: client-only dapat mengurangi manfaat SSR untuk konten utama
- Performa awal: SSR biasanya memberi HTML awal lebih cepat terbaca
- Kompleksitas engineering: menstabilkan pipeline SSR butuh disiplin transformasi
- Keandalan UI: client-only kadang lebih sederhana untuk editor kompleks
Untuk kebanyakan kasus, kombinasi SSR viewer + client-only editor/enhancer adalah titik tengah yang paling praktis.
Kesalahan yang sering terjadi
- Menyanitasi HTML di server lalu mengubahnya lagi di client
- Menghasilkan ID dari random atau timestamp
- Memformat tanggal saat render awal
- Menganggap browser akan mempertahankan string HTML mentah apa adanya
- Memakai plugin editor yang memodifikasi DOM dalam subtree SSR
- Mengabaikan warning hydration karena halaman "terlihat normal"
Ringkasan praktik yang disarankan
- Siapkan HTML final di server atau pipeline backend
- Gunakan sanitasi dan normalisasi yang konsisten
- Pastikan ID, atribut, dan struktur markup deterministik
- Hindari nilai berbasis waktu atau random saat SSR
- Render viewer HTML sesederhana mungkin
- Pisahkan editor interaktif atau enhancer DOM menjadi client-only bila perlu
- Uji di dev dan production build dengan fixture konten kompleks
Jika Anda menangani HTML hasil Markdown, rich text editor, atau output siap-paste yang kompleks, tujuan utamanya bukan membuat SSR terlihat bekerja, tetapi memastikan bahwa markup yang dihydrate benar-benar sama. Begitu output final stabil, sebagian besar hydration mismatch di Next.js akan hilang tanpa perlu workaround yang rapuh.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!