Hydration error pada aplikasi React atau Next.js terjadi ketika struktur Document Object Model (DOM) hasil render server berbeda dengan representasi DOM virtual awal yang dihasilkan klien. Masalah ini sering muncul saat aplikasi merender antarmuka dinamis (Dynamic UI) dari output tool calls AI Agent, seperti respons terstruktur JSON atau skema komponen dinamis.

Akar Masalah Hydration Mismatch pada Dynamic Tool UI

Tiga faktor utama pemicu desinkronisasi DOM antara server dan browser saat memproses output AI tool:

  • Non-deterministic Schema Parsing: Schema parser (misalnya Zod atau JSON parsing custom) mengeksekusi transformasi data yang bergantung pada environment, seperti parsing format tanggal lokal atau instansiasi ID acak non-deterministik (crypto.randomUUID() atau Math.random()) selama render.
  • Partial Chunk Streaming: Tool execution stream AI sering kali belum tuntas saat server memulai SSR streaming (misal via renderToPipeableStream). Server memproses snapshot data yang tidak identik dengan data yang diterima klien saat hidrasi dimulai.
  • Browser-Specific Runtime Drift: Tool call UI sering mengakses properti seperti window.innerWidth, preferensi tema, atau navigator.language secara inline saat proses validasi skema komponen.

Pola Solusi 1: Isomorphic State Snapshot

Pastikan state skema hasil tool call di-freeze dalam snapshot serializable yang identik sebelum tree dirender. Hindari sanitasi data dinamis di dalam bodi komponen.

// types/tool.ts
export type ToolExecutionPayload = {
  toolCallId: string;
  name: string;
  rawArgs: Record<string, unknown>;
  timestamp: number; // Gunakan epoch UTC dari backend, jangan Date.now() di render
};

// components/DynamicToolRenderer.tsx
import React, { useId } from 'react';

export function DynamicToolRenderer({ payload }: { payload: ToolExecutionPayload }) {
  // Gunakan useId native React untuk anchor DOM unik yang konsisten antara SSR dan hydration
  const instanceId = useId();

  if (payload.name === 'weatherWidget') {
    const temp = Number(payload.rawArgs.temp ?? 0);
    const city = String(payload.rawArgs.city ?? 'Unknown');

    return (
      <div id={instanceId} className="border p-4 rounded-md">
        <h3 className="font-bold">{city}</h3>
        <p>{temp}&deg;C</p>
        <time dateTime={new Date(payload.timestamp).toISOString()}>
          {new Date(payload.timestamp).toISOString()}
        </time>
      </div>
    );
  }

  return <pre>{JSON.stringify(payload.rawArgs, null, 2)}</pre>;
}

Pola Solusi 2: Lazier Rendering Boundary

Jika output tool AI Agent memuat dependensi client-only atau parsing schema yang menghasilkan layout dinamis tak stabil di server, isolasi elemen tersebut dengan Dynamic Component Boundary tanpa SSR via Next.js Dynamic Imports atau pattern useSyncExternalStore.

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

import dynamic from 'next/dynamic';
import React from 'react';
import type { ToolExecutionPayload } from '@/types/tool';

// Isolasi komponen rendering eksekusi AI dari SSR pipeline
const DynamicToolRenderer = dynamic(
  () => import('./DynamicToolRenderer').then((mod) => mod.DynamicToolRenderer),
  {
    ssr: false,
    loading: () => <div className="h-24 w-full animate-pulse bg-gray-100 rounded" />,
  }
);

export function DeferredToolContainer({ payload }: { payload: ToolExecutionPayload }) {
  // Server hanya merender placeholder skeleton. Klien merender output tool setelah mount.
  return <DynamicToolRenderer payload={payload} />;
}

Verifikasi dan Debugging Run-time

Untuk mendeteksi node yang memicu perbedaan DOM tanpa bergantung pada stack trace yang terkompresi:

  1. Periksa Atribut Mismatch: React 19 dan Next.js App Router menyajikan log visual string SSR vs Client DOM diff di browser DevTools. Teliti atribut id, string tanggal, atau tag bersarang yang salah (seperti <p> di dalam <p> akibat output Markdown parser).
  2. Hindari useEffect Flag: Menggunakan useEffect dan isMounted state boolean menyebabkan double-render layout shift dan berpotensi flash-of-unstyled-content. Gunakan teknik next/dynamic dengan ssr: false atau useSyncExternalStore untuk mendeteksi runtime browser secara konsisten.
Catatan: Selalu lakukan serialisasi schema tool args di backend atau Route Handler sebelum mengirimkannya ke stream React Server Components. Jangan menjalankan kalkulasi non-deterministik di fase rendering komponen UI.