Hydration mismatch terjadi ketika representasi Virtual DOM hasil server-side rendering (SSR) tidak cocok dengan render pertama di browser client. Pada UI chatbot berbasis LLM streaming di Next.js atau React, error seperti Hydration failed because the initial UI does not match what was rendered on the server sering muncul saat response teks mengalir ke DOM.
Penyebab Utama Hydration Mismatch pada Response AI
Tiga faktor utama menyebabkan desinkronisasi render pada antarmuka streaming:
- Desinkronisasi State Chunk: Komponen membaca state streaming yang sudah terisi di client (misalnya dari cache lokal atau query stream yang langsung berjalan), sementara server merender state kosong atau parsial.
- Parser Markdown dan Token Delimiter Rusak: Chunk stream sering memotong token Markdown di tengah (seperti
```tyatau**te). Library Markdown client mencoba memperbaiki tag yang tidak lengkap, menghasilkan struktur HTML berbeda dari HTML polos yang dirender server. - Manipulasi DOM Langsung: SDK runtime pihak ketiga memanipulasi
element.innerHTMLsebelum hydration React selesai, merusak rekonsiliasi VDOM.
Solusi 1: Isolasi Komponen Stream dengan Fallback Deterministik
Hindari merender teks streaming dinamis langsung pada SSR jika konten tersebut hanya hidup di sesi interaktif pengguna. Gunakan dynamic import dengan flag ssr: false atau pasang guard berbasis state terpasang (mounted).
import dynamic from 'next/dynamic';
const DynamicChatStream = dynamic(
() => import('./ChatStreamView'),
{
ssr: false,
loading: () => <div className="chat-skeleton">Memuat percakapan...</div>,
}
);Solusi 2: Normalisasi State Stream dengan useSyncExternalStore
Penggunaan useEffect untuk sinkronisasi stream memicu re-render ganda dan rentan race condition. useSyncExternalStore memastikan snapshot server dan initial client snapshot identik, menghindari mismatch saat stream masuk.
import { useSyncExternalStore } from 'react';
class StreamStore {
private text = '';
private listeners = new Set<() => void>();
subscribe = (listener: () => void) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
getSnapshot = () => this.text;
getServerSnapshot = () => ''; // Selalu kosong pada render SSR deterministik
appendChunk = (chunk: string) => {
this.text += chunk;
this.listeners.forEach((l) => l());
};
reset = () => {
this.text = '';
this.listeners.forEach((l) => l());
};
}
export const chatStore = new StreamStore();
export function useChatStream() {
return useSyncExternalStore(
chatStore.subscribe,
chatStore.getSnapshot,
chatStore.getServerSnapshot
);
}Normalisasi Chunk Buffer Sebelum Render Markdown
Parsing Markdown parsial langsung ke HTML memicu perubahan struktur DOM yang merusak hydration. Tangani buffering delimiter Markdown sebelum komponen merender markup.
// Sanitasi sederhana untuk mencegah tag gantung memicu modifikasi DOM agresif
export function sanitizeStreamingBuffer(rawText: string): string {
// Hitung jumlah block delimiter code triple backtick
const codeBlockCount = (rawText.match(/```/g) || []).length;
// Jika delimiter ganjil, tambahkan penutup sementara untuk rendering aman
if (codeBlockCount % 2 !== 0) {
return `${rawText}\n\`\`\``;
}
return rawText;
}Runnable Unit Test: Buffer Parser Validation
Script verifikasi mandiri tanpa framework test untuk memastikan normalisasi chunk menutup block code yang tidak lengkap.
function assert(condition: boolean, message: string) {
if (!condition) throw new Error(`Assertion failed: ${message}`);
}
// Run test
const incompleteStream = "Contoh script:\n```typescript\nconst a = 1;";
const fixedStream = sanitizeStreamingBuffer(incompleteStream);
assert(
fixedStream.endsWith("```"),
"Stream belum tertutup harus ditutup dengan triple backtick"
);
const balancedStream = "Code:\n```js\nlet x = 10;\n```";
assert(
sanitizeStreamingBuffer(balancedStream) === balancedStream,
"Stream lengkap tidak boleh dimodifikasi"
);
console.log("Semua pengujian sanitasi buffer lolos.");Checklist Mitigasi Error React 18/19
- Pastikan render pertama di browser client menggunakan snapshot persis sama dengan
renderToStringserver. - Tunda eksekusi inisiasi stream SDK client hingga komponen selesai mount.
- Gunakan
suppressHydrationWarninghanya pada elemen teks spesifik jika terdapat perbedaan tak terhindarkan seperti penanda waktu (timestamp).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!