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 ```ty atau **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.innerHTML sebelum 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 renderToString server.
  • Tunda eksekusi inisiasi stream SDK client hingga komponen selesai mount.
  • Gunakan suppressHydrationWarning hanya pada elemen teks spesifik jika terdapat perbedaan tak terhindarkan seperti penanda waktu (timestamp).