Akar Masalah Hydration Mismatch dari Oracle DATE

Hydration mismatch terjadi ketika representasi DOM hasil server-side rendering (SSR) berbeda dengan representasi DOM yang dibangun oleh JavaScript di browser. Salah satu pemicu paling umum pada arsitektur Node.js dan Oracle Database adalah serialisasi tipe DATE dan TIMESTAMP.

Oracle DATE menyimpan tahun, bulan, hari, jam, menit, dan detik tanpa komponen zona waktu eksplisit. Saat driver node-oracledb membaca kolom DATE, driver mengonversinya menjadi objek Date bawaan JavaScript menggunakan zona waktu lokal runtime Node.js atau pengaturan NLS sesi database. Ketika objek Date tersebut diserialisasi ke JSON payload SSR:

  • Server Node.js berjalan di zona waktu UTC atau waktu server (misal: +00:00), menghasilkan string ISO atau teks waktu tertentu.
  • Browser klien mengeksekusi hydration di zona waktu pengguna (misal: WIB/+07:00).
  • DOM yang di-render di server bertuliskan 10:00, sedangkan hasil evaluasi klien bertuliskan 17:00. Hydration gagal dan memicu rendering error.

Solusi 1: Enforcement TIME_ZONE pada Pool Koneksi node-oracledb

Secara default, sesi Oracle mewarisi konfigurasi waktu dari database atau environment OS host. Untuk memastikan server Node.js selalu menerima representasi waktu yang deterministik, paksa sesi Oracle menggunakan zona waktu UTC via sessionCallback pada connection pool.

const oracledb = require('oracledb');

async function initializePool() {
  await oracledb.createPool({
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    connectString: process.env.DB_CONNECT_STRING,
    sessionCallback: async (conn) => {
      // Pastikan semua sesi koneksi menggunakan basis waktu UTC seragam
      await conn.execute("ALTER SESSION SET TIME_ZONE = '+00:00'");
    },
    poolMin: 2,
    poolMax: 10
  });
}

Pendekatan ini menjamin pembacaan tipe data bertipe TIMESTAMP WITH LOCAL TIME ZONE dikonversi seragam ke basis UTC sebelum masuk ke Node.js process.

Solusi 2: oracledb.fetchAsString untuk Menghindari Instansiasi Date JS

Mengizinkan node-oracledb mengubah DATE menjadi JavaScript Date membuat data rentan terhadap offset lokal V8 engine. Alternatif paling efisien adalah memaksa driver mengembalikan tipe DATE langsung sebagai string.

const oracledb = require('oracledb');

// Set global fetch handling
oracledb.fetchAsString = [oracledb.DATE];

async function getOrders(connection) {
  const result = await connection.execute(
    `SELECT order_id, order_date FROM orders WHERE ROWNUM <= 10`,
    [],
    { outFormat: oracledb.OUT_FORMAT_OBJECT }
  );
  return result.rows;
}

Properti order_date akan berupa string murni sesuai format NLS sesi database, tanpa overhead alokasi memori untuk objek Date JS di backend.

Solusi 3: Standardisasi Format ISO 8601 UTC di SQL Query

Solusi paling aman untuk aplikasi SSR adalah delegasi pemformatan tanggal langsung ke SQL engine Oracle menggunakan TO_CHAR dengan pola standar ISO 8601 (UTC). Format ini dapat diparsing secara deterministik oleh frontend tanpa terpengaruh parameter NLS server.

SELECT
  order_id,
  -- Jika data disimpan dalam waktu lokal server Oracle, konversi eksplisit ke UTC
  TO_CHAR(
    SYS_EXTRACT_UTC(FROM_TZ(CAST(order_date AS TIMESTAMP), 'Asia/Jakarta')),
    'YYYY-MM-DD"T"HH24:MI:SS"Z"'
  ) AS order_date_iso
FROM orders;

Jika kolom sudah bertipe TIMESTAMP WITH TIME ZONE atau disimpan murni dalam UTC:

SELECT
  order_id,
  TO_CHAR(created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at_iso
FROM orders;

Implementasi Konsisten di Sisi Frontend (React/SSR)

Setelah backend mengirim string ISO 8601 UTC murni (misal: 2026-03-31T14:30:00Z), cegah evaluasi zona waktu lokal yang berbeda saat initial render di SSR.

Opsi A: Format Deterministik dengan Explicit Timezone

Paksa formatter menggunakan zona waktu tertentu di server dan client:

export function OrderDate({ isoString }) {
  // ponytail: format statis berbasis UTC seragam di server & browser
  const formatted = new Intl.DateTimeFormat('id-ID', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: 'UTC'
  }).format(new Date(isoString));

  return <time dateTime={isoString}>{formatted} UTC</time>;
}

Opsi B: Dua Tahap Rendering (Two-Pass Rendering)

Jika UI wajib menampilkan waktu lokal browser pengguna (WIB/WITA/WIT), hindari merender format lokal saat SSR pass pertama:

import { useState, useEffect } from 'react';

export function LocalTime({ isoString }) {
  const [renderedDate, setRenderedDate] = useState(isoString);

  useEffect(() => {
    // Berjalan hanya di client setelah hydration selesai
    setRenderedDate(new Date(isoString).toLocaleString('id-ID'));
  }, [isoString]);

  return <time dateTime={isoString}>{renderedDate}</time>;
}

Ringkasan Keputusan Teknis

  • Tingkat Database/Driver: Konfigurasi oracledb.fetchAsString = [oracledb.DATE] dan tetapkan TIME_ZONE = '+00:00' pada connection pool.
  • Tingkat Query: Gunakan TO_CHAR(..., 'YYYY-MM-DD"T"HH24:MI:SS"Z"') agar serialisasi data ke JSON SSR stabil.
  • Tingkat Frontend: Hindari kalkulasi waktu lokal user langsung pada first-render tree jika nilai tersebut bergantung pada environment client.