Hydration error terjadi ketika DOM tree hasil render awal di browser tidak cocok dengan markup HTML yang dikirim oleh server. Masalah ini sering muncul saat membangun dashboard web yang merender output terminal subprocess dari CLI interceptor seperti mimic via Server-Side Rendering (SSR). Ketidaksesuaian karakter byte per byte merusak pohon hidrasi React atau framework SSR modern lainnya.

Penyebab Utama Hydration Mismatch pada Output CLI

Saat server mengeksekusi interceptor proses (seperti littledivy/mimic) dan langsung memasukkan buffernya ke payload render, tiga faktor utama memicu hydration mismatch:

  • Perbedaan Newline Platform (CRLF vs LF): Windows menggunakan \r\n sementara lingkungan Linux/macOS memakai \n. Parser HTML browser sering menormalisasi teks node secara otomatis, sementara server menyajikan raw stream.
  • ANSI Escape Sequences: Kode warna terminal dan cursor positioning (seperti \x1b[32m) diproses berbeda jika library parser ANSI di sisi klien menghasilkan virtual DOM yang berbeda dibanding server.
  • Timing Execution Subprocess: Output CLI yang diambil saat request SSR menghasilkan snapshot data yang tidak identik jika browser melakukan sinkronisasi ulang state sebelum hidrasi selesai.

Sanitasi Output CLI di Sisi Server

Solusi pertama adalah normalisasi buffer teks sebelum data masuk ke template SSR. Normalisasi newline dan pembersihan ANSI harus deterministik.

// lib/terminal-sanitizer.ts
const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;

export function sanitizeTerminalOutput(rawOutput: string): string {
  // 1. Normalisasi CRLF ke LF
  const normalizedLineEndings = rawOutput.replace(/\r\n|\r/g, '\n');
  
  // 2. Strip control character dan ANSI sequence jika render raw text
  return normalizedLineEndings.replace(ANSI_REGEX, '');
}

Isolasi Terminal Wrapper dengan Dynamic Import

Jika UI membutuhkan rendering full terminal emulator (seperti Xterm.js atau renderer ANSI-to-HTML), hindari rendering komponen tersebut di server. Gunakan dynamic import dengan flag ssr: false untuk mengisolasi DOM terminal dari fase hidrasi awal.

// components/terminal-view.tsx
'use client';

import dynamic from 'next/dynamic';

const TerminalRenderer = dynamic(
  () => import('./TerminalRendererClient'),
  {
    ssr: false,
    loading: () => <div className="terminal-skeleton">Loading session...</div>,
  }
);

export default function TerminalView({ initialBuffer }: { initialBuffer: string }) {
  return (
    <div className="terminal-wrapper">
      <TerminalRenderer buffer={initialBuffer} />
    </div>
  );
}

Sinkronisasi State Streaming stdout dan stderr

Untuk streaming output CLI real-time, pisahkan snapshot data awal yang dipakai server dengan stream runtime. Simpan buffer statis di server, dan mulai konsumsi WebSocket atau Server-Sent Events (SSE) hanya di dalam lifecycle hook klien.

// components/TerminalRendererClient.tsx
'use client';

import { useState, useEffect } from 'react';

export default function TerminalRendererClient({ buffer }: { buffer: string }) {
  const [output, setOutput] = useState(buffer);

  useEffect(() => {
    // Buka koneksi streaming hanya setelah hidrasi selesai
    const eventSource = new EventSource('/api/cli/stream');
    
    eventSource.onmessage = (event) => {
      setOutput((prev) => prev + event.data);
    };

    return () => eventSource.close();
  }, []);

  return <pre className="terminal-window"><code>{output}</code></pre>;
}
Catatan Debugging: Jika hydration mismatch tetap muncul, periksa wrapper tag. Penggunaan tag <pre> di dalam <p> melanggar HTML spec dan otomatis diubah oleh browser, menyebabkan struktur DOM berbeda dari SSR output.