Penyebab Hydration Error pada Dokumen Word (.docx)
Hydration error di React dan Next.js terjadi saat DOM tree hasil Server-Side Rendering (SSR) tidak cocok secara biner atau struktural dengan DOM tree yang dibentuk browser pada siklus render pertama. Saat mem-parsing dokumen .docx (misalnya via Mammoth, docx-preview, atau custom OpenXML parser), selisih representasi ini kerap muncul akibat empat faktor:
- Invalid HTML Nesting dari Word XML: Spesifikasi WordprocessingML mengizinkan nested run (
<w:r>) dan paragraph properties yang jika dikonversi secara naif menghasilkan elemen seperti<p>di dalam<p>, atau blok<div>di dalam inline text. Browser otomatis memperbaiki (auto-correct) DOM tree ini saat parsing HTML mentah di sisi client, memicu mismatch seketika terhadap output server. - Perbedaan Eksekusi XMLParser/DOMParser: Server (Node.js) sering mengandalkan parser berbasis regex atau pustaka seperti
xmldom, sedangkan browser menggunakanDOMParserengine bawaan (Blink/Gecko). Keduanya menangani self-closing tags yang non-standar (misal<w:br/>atau<span />) secara berbeda. - Whitespace Collapsing: Word mempertahankan representasi spasi via
xml:space="preserve". Tanpa normalisasi ketat, serialisasi string server bisa membuang consecutive whitespace atau newline yang oleh browser dipertahankan atau diubah strukturnya ke standard text node. - Metadata & Timestamp Deterministik: Parser sering mengekstrak metadata dokumen seperti
lastModifiedataucreated. Jika format tanggal menggunakan locale lokal client atau runtime tanpa zona waktu UTC eksplisit, string waktu render server dan client akan berbeda.
Reproduksi Masalah: Contoh Komponen yang Gagal
Kode di bawah mendemonstrasikan komponen Next.js yang memicu React Hydration Error karena menyuntikkan HTML hasil parse .docx mentah secara langsung ke dalam DOM tree tanpa normalisasi:
// components/DocxViewerBuggy.tsx
import React from 'react';
interface Props {
rawWordHtml: string;
lastModified: Date;
}
export function DocxViewerBuggy({ rawWordHtml, lastModified }: Props) {
return (
<div className="document-preview">
<div className="meta">
<!-- Mismatch 1: Localized date render berbeda di server (UTC) vs browser (WIB) -->
Diperbarui: {lastModified.toLocaleDateString()}
</div>
<!-- Mismatch 2: rawWordHtml berisi tag ilegal, nested <p>, atau unclosed self-closing tag -->
<p dangerouslySetInnerHTML={{ __html: rawWordHtml }} />
</div>
);
}Jika rawWordHtml berisi string "<p>Paragraf 1</p><table>...</table>", HTML parser browser akan mengeluarkan tag <table> dari <p> terluar karena standar HTML melarang tabel di dalam paragraf. Virtual DOM React di browser mendeteksi struktur root yang berbeda dari output SSR, memicu peringatan: "Hydration failed because the initial UI does not match what was rendered on the server."
Solusi 1: Normalisasi dan Sanitasi Deterministik di Server
Jika preview harus di-render di server untuk kebutuhan SEO atau performa, parsing string harus melewati tahap sanitasi dan validasi schema HTML ketat sebelum React merendernya. Gunakan DOMPurify yang dijalankan di server dengan factory deterministik.
// lib/docx-normalizer.ts
import sanitizeHtml from 'sanitize-html';
export function normalizeDocxHtml(rawHtml: string): string {
return sanitizeHtml(rawHtml, {
allowedTags: ['p', 'span', 'b', 'i', 'strong', 'em', 'table', 'tbody', 'tr', 'td', 'th', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
allowedAttributes: {
'*': ['style', 'class'],
td: ['colspan', 'rowspan'],
th: ['colspan', 'rowspan'],
},
// Mencegah parsing tag ilegal di dalam tag inline
disallowedTagsMode: 'discard',
transformTags: {
// Paksa konversi inline self-closing yang rusak
span: (tagName, attribs) => ({
tagName,
attribs,
}),
},
}).trim();
}Solusi 2: Client-Only Dynamic Import untuk Browser-Specific Parser
Library seperti docx-preview bergantung sepenuhnya pada Canvas API dan document.createElement. Solusi paling aman untuk engine parser jenis ini adalah mengisolasi render ke Client Component tanpa tahap SSR menggunakan opsi ssr: false pada next/dynamic.
// components/DocxContainer.tsx
'use client';
import dynamic from 'next/dynamic';
// Lazy load komponen preview tanpa SSR
const DocxRenderer = dynamic(
() => import('./DocxRendererClient').then((mod) => mod.DocxRendererClient),
{
ssr: false,
loading: () => <div className="p-4 text-gray-500">Memuat dokumen...</div>,
}
);
export default function DocxContainer({ fileUrl }: { fileUrl: string }) {
return (
<section className="docx-wrapper">
<DocxRenderer fileUrl={fileUrl} />
</section>
);
}// components/DocxRendererClient.tsx
'use client';
import React, { useEffect, useRef } from 'react';
import { renderAsync } from 'docx-preview';
interface Props {
fileUrl: string;
}
export function DocxRendererClient({ fileUrl }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let isMounted = true;
async function loadDocument() {
const res = await fetch(fileUrl);
const blob = await res.blob();
if (containerRef.current && isMounted) {
containerRef.current.innerHTML = ''; // Reset container
await renderAsync(blob, containerRef.current);
}
}
loadDocument().catch(console.error);
return () => {
isMounted = false;
};
}, [fileUrl]);
return <div ref={containerRef} className="docx-document-canvas" />;
}Pengujian Otomatis untuk Mencegah Hydration Mismatch
Jalankan assertion test deterministik untuk memastikan parser server dan DOM output browser tetap identik. Gunakan test runner standar (Node.js native runner atau Jest) bersama jsdom untuk menguji sanitasi:
// tests/docx-parser.test.ts
import assert from 'node:assert/strict';
import test from 'node:test';
import { JSDOM } from 'jsdom';
import { normalizeDocxHtml } from '../lib/docx-normalizer';
test('Struktur HTML hasil parse Word harus stabil dan valid', () => {
const dirtyDocxOutput = '<p>Teks awal<p>Nested Paragraf Word</p></p><span/>';
const cleanHtml = normalizeDocxHtml(dirtyDocxOutput);
const dom = new JSDOM(`<!DOCTYPE html><div id="root">${cleanHtml}</div>`);
const rootElement = dom.window.document.getElementById('root');
// Verifikasi tidak ada penutupan auto-repair yang merusak hierarki tree
assert.equal(rootElement?.children.length, 2);
assert.equal(rootElement?.getElementsByTagName('p')[0].children.length, 0);
});Checklist Implementasi
- Hindari
dangerouslySetInnerHTMLdi dalam tag<p>: Gunakan selalu pembungkus netral seperti<div>atau<article>. - Standarisasi Format Waktu: Format tanggal dokumen di server menggunakan format UTC atau ISO 8601 eksplisit sebelum dikirim ke UI.
- Pilih Rendering Strategy: Gunakan sanitasi AST/HTML jika membutuhkan SSR, atau gunakan dynamic import (
ssr: false) jika mengandalkan DOM API penuh untuk visualisasi presisi tata letak dokumen Word.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!