Hydration error terjadi saat pohon DOM yang dihasilkan oleh server-side rendering (SSR) berbeda dari render pertama di client engine React. Pada UI visualizer workflow agent—khususnya sistem berbasis loop seperti LangGraph, AutoGen, atau custom LLM runner—masalah ini kerap muncul akibat sifat asinkron dan non-deterministik dari eksekusi model AI.
Ketika server mengirimkan snapshot awal sementara client langsung mengonsumsi stream atau memanipulasi graph state sebelum hidrasi tuntas, browser melempar Hydration failed because the initial UI does not match what was rendered on the server. Artikel ini membahas akar penyebab teknis dan pola arsitektur untuk mengatasinya.
Penyebab Utama Hydration Mismatch pada Agent Loop
1. Streaming Token LLM Mendahului Client Mount
Server merender initial state di mana stream agent belum berjalan atau baru mencapai node awal. Jika client menginisialisasi WebSocket atau Server-Sent Events (SSE) langsung di level komponen tanpa menunggu lifecycle mount, state agent loop akan terisi token baru sebelum hidrasi selesai. Akibatnya, teks atau status eksekusi pada client berbeda dari markup HTML kiriman server.
2. Non-Deterministic ID pada Graph Visualizer
Workflow visualizer sering kali membuat node dinamis untuk setiap iterasi loop menggunakan generator acak seperti Math.random(), crypto.randomUUID(), atau timestamp. ID yang dievaluasi di server tidak akan pernah sama dengan ID yang dihitung ulang di client.
3. Akses Storage / Browser API pada Initial Render
Mengambil cache eksekusi agent dari localStorage atau mengecek ukuran canvas DOM di root component body menyebabkan cabang render (conditional branch) server dan client tidak sinkron.
Contoh Kasus: Komponen Agent Loop Bermasalah
Berikut adalah contoh komponen visualizer agent loop yang memicu hydration error karena pembuatan ID acak dan inisialisasi stream langsung saat pemanggilan fungsi komponen:
// AgentWorkflowBad.tsx
import React, { useState } from "react";
interface Node {
id: string;
name: string;
}
export function AgentWorkflowBad({ initialStep }: { initialStep: string }) {
// MASALAH 1: crypto.randomUUID() menghasilkan nilai berbeda di Server vs Client
const [nodes] = useState<Node[]>([
{ id: crypto.randomUUID(), name: initialStep }
]);
// MASALAH 2: Membaca cache client sebelum mount
const cachedStatus = typeof window !== "undefined"
? localStorage.getItem("agent_status") || "IDLE"
: "IDLE";
return (
<div className="workflow-container">
<span>Status: {cachedStatus}</span>
<ul>
{nodes.map((node) => (
<li key={node.id} id={node.id}>{node.name}</li>
))}
</ul>
</div>
);
}Solusi Perbaikan: Deterministic State dan Isolasi Lifecycle
Untuk menstabilkan rendering, pisahkan tanggung jawab rendering menjadi dua fase: initial deterministic snapshot (fase hidrasi) dan runtime interactive streaming (post-mount).
1. Gunakan React useId untuk Node Deterministik
React menyediakan hook useId yang menjamin ID identik dihasilkan di server dan client sepanjang tree structure komponen sama.
2. Pola Two-Pass Rendering untuk State Lokal Client
Render versi statis/fallback pada pass pertama. Lakukan sinkronisasi state lokal (seperti pembacaan storage atau listener koneksi loop) di dalam useEffect yang hanya dieksekusi di client setelah hidrasi tuntas.
Kode Perbaikan
// AgentWorkflowFixed.tsx
"use client";
import React, { useState, useEffect, useId } from "react";
interface AgentWorkflowProps {
initialStep: string;
runId: string; // Inject ID stabil dari database/props, bukan acak di render
}
export function AgentWorkflowFixed({ initialStep, runId }: AgentWorkflowProps) {
const baseId = useId();
const [mounted, setMounted] = useState(false);
const [status, setStatus] = useState<string>("INITIALIZING");
// Tunda pembacaan state client hingga hidrasi selesai
useEffect(() => {
setMounted(true);
const storedStatus = localStorage.getItem(`agent_${runId}`) || "READY";
setStatus(storedStatus);
// Inisialisasi SSE / WebSocket agent loop di sini
const eventSource = new EventSource(`/api/agent/stream?runId=${runId}`);
eventSource.onmessage = (event) => {
const payload = JSON.parse(event.data);
setStatus(payload.status);
};
return () => eventSource.close();
}, [runId]);
// SSR dan First Client Pass merender UI yang identik
if (!mounted) {
return (
<div className="workflow-container">
<span>Status: INITIALIZING</span>
<ul>
<li id={`${baseId}-step-0`}>{initialStep}</li>
</ul>
</div>
);
}
// Render interaktif setelah hidrasi aman
return (
<div className="workflow-container">
<span>Status: {status}</span>
<ul>
<li id={`${baseId}-step-0`}>{initialStep}</li>
</ul>
</div>
);
}Pendekatan Alternatif: Client-Side Only Boundary
Jika visualizer visual workflow agent terlalu kompleks (misalnya menggunakan canvas WebGL atau ReactFlow yang sarat manipulasi DOM langsung), jangan paksakan SSR untuk keseluruhan komponen visualizer. Bungkus komponen tersebut menggunakan dynamic import dengan SSR dimatikan pada framework seperti Next.js:
import dynamic from 'next/dynamic';
const DynamicAgentVisualizer = dynamic(
() => import('./AgentGraphVisualizer').then((mod) => mod.AgentGraphVisualizer),
{
ssr: false,
loading: () => <div className="skeleton-loader">Loading Agent Canvas...</div>
}
);Catatan Desain: Matikan SSR hanya pada visualizer graph kanvas runtime. Biarkan metadata, log audit eksekusi teks, dan kontrol agent tetap di-render via SSR demi performa Core Web Vitals (LCP) dan SEO.
Checklist Debugging Hydration Agent Loop
- Pastikan tidak ada eksekusi
Math.random()atauDate.now()di root render function komponen. - Pastikan stream WebSocket atau listener SSE tidak mengupdate React state sebelum event
componentDidMount/useEffectdipicu. - Verifikasi bahwa output initial state dari endpoint API SSR cocok dengan props hydration client.
- Gunakan flag
suppressHydrationWarninghanya pada elemen teks spesifik yang tidak kritis (misal: timestamp real-time) dan hindari penggunaannya pada structural wrapping tag.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!