Hydration mismatch pada aplikasi React dengan Server-Side Rendering (SSR) terjadi saat struktur Virtual DOM dari server tidak identik dengan hasil eksekusi render pertama di browser. Pada dashboard finansial dan analitik saham berkecepatan tinggi (seperti arsitektur panel tick data), error Hydration failed because the initial UI does not match what was rendered on the server sering muncul akibat manipulasi timestamp, presisi desimal mesin analitik, dan state data real-time.
Akar Masalah Hydration Error pada Tick Data
Data finansial memiliki karakteristik spesifik yang memicu divergensi antara Node.js runtime di server dan browser client:
- Divergensi Timezone dan Format Tanggal: Server production umumnya berjalan di environment UTC (misalnya container Docker Linux), sedangkan browser pengguna berjalan pada timezone lokal (misalnya GMT+7 / WIB). Panggilan seperti
new Date(tick.timestamp).toLocaleTimeString()menghasilkan string berbeda di server dan client. - Serialisasi Angka Floating-Point: Pipeline analitik berbasis OLAP berkinerja tinggi seperti DuckDB atau Polars mengekspor angka presisi ganda (IEEE 754 float64/double). Serialisasi JSON ke JavaScript number sering mengalami perbedaan representasi rounding atau format representasi locale (misalnya tanda titik vs koma) pada
Intl.NumberFormat. - Inisialisasi Charting Library: Library canvas atau SVG seperti Lightweight Charts atau ECharts mengakses objek
windowatau mengukur dimensi layout DOM (clientWidth) yang belum tersedia saat proses SSR berlangsung.
1. Standardisasi ISO Timestamp dan Timezone Terkunci
Hindari pemformatan tanggal dinamis langsung di root rendering tree SSR. Standardisasikan format waktu ke representasi UTC eksplisit jika dirender di server, atau isolasi locale display ke client.
// utils/formatters.ts
export function formatTickTime(timestampMs: number, timeZone = 'UTC'): string {
return new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hourCycle: 'h23',
timeZone,
}).format(new Date(timestampMs));
}Komponen UI harus menggunakan prop timezone statis atau menunda resolusi timezone lokal pengguna hingga tahap client mount.
2. Isolasi Komponen Chart via Dynamic Import SSR: False
Komponen grafik finansial dan order book berbasis DOM layout tidak boleh di-render di server. Gunakan dynamic import Next.js untuk memotong eksekusi SSR pada level tree tersebut.
// components/StockChartPanel.tsx
import dynamic from 'next/dynamic';
interface ChartProps {
ticker: string;
initialData: Array<{ time: number; price: number; volume: number }>;
}
export const StockChart = dynamic<ChartProps>(
() => import('./ClientChartCanvas').then((mod) => mod.ClientChartCanvas),
{
ssr: false,
loading: () => <div className="chart-skeleton h-[400px] bg-slate-900 animate-pulse" />,
}
);3. Pola Two-Pass Rendering Minimal untuk State Client
Untuk komponen teks tick data yang wajib menampilkan waktu lokal browser (misalnya panel Order Execution), terapkan two-pass render minimal. Hindari rendering markup yang berbeda sebelum efek mount dieksekusi.
// components/TickRow.tsx
'use client';
import { useState, useEffect } from 'react';
import { formatTickTime } from '@/utils/formatters';
interface TickRowProps {
symbol: string;
price: number;
timestamp: number;
}
export function TickRow({ symbol, price, timestamp }: TickRowProps) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
// ponytail: upgrade ke useSyncExternalStore jika komponen membutuhkan sinkronisasi state global
setIsClient(true);
}, []);
return (
<tr>
<td>{symbol}</td>
<td>{price.toFixed(4)}</td>
<td>{isClient ? formatTickTime(timestamp, Intl.DateTimeFormat().resolvedOptions().timeZone) : formatTickTime(timestamp, 'UTC')}</td>
</tr>
);
}4. Pemisahan Initial Snapshot vs WebSocket Stream
Kesalahan fatal lainnya adalah menyambungkan koneksi WebSocket langsung sebelum hydration selesai. Jika pesan tick masuk dan memutasi state React sebelum runtime client selesai melakukan rekonsiliasi initial HTML dari server, hydration tree rusak seketika.
Pisahkan fase ingest data menjadi dua jalur: server menyediakan static snapshot terhitung (misalnya query terakhir dari DuckDB), dan client mengaktifkan live ingestion hanya setelah mount.
// hooks/useTickStream.ts
'use client';
import { useEffect, useState } from 'react';
export interface Tick {
price: number;
time: number;
}
export function useTickStream(symbol: string, initialSnapshot: Tick[]) {
const [data, setData] = useState<Tick[]>(initialSnapshot);
useEffect(() => {
// Inisialisasi koneksi transport data pasar HANYA setelah hydration selesai
const ws = new WebSocket(`wss://marketdata.local/stream?symbol=${symbol}`);
ws.onmessage = (event) => {
const incoming: Tick = JSON.parse(event.data);
setData((prev) => [...prev.slice(-99), incoming]);
};
return () => {
ws.close();
};
}, [symbol]);
return data;
}Catatan Serialisasi Desimal: Jika data diproses melalui backend Python/Polars atau database analitik embedded, serialisasikan nilai numerik sepertispreadataubid_asksebagai fixed string di API boundary. Parsing manual di layer UI mencegah variasi representasi0.10000000000000002antara engine V8 server dan webview client.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!