Hydration mismatch terjadi saat pohon DOM yang dihasilkan oleh kompilasi sisi server (SSR atau template HTML pra-render) tidak identik dengan pohon DOM hasil render pertama di sisi klien (React atau Vue). Dalam arsitektur hybrid di mana Spring Boot bertindak sebagai penyedia initial state—baik melalui rendering template Thymeleaf, JTE, maupun SSR bridge seperti Node.js subprocess—perbedaan output serialisasi JSON adalah penyebab utama inkonsistensi rendering ini.
Root Cause: Mengapa Serialisasi Jackson Memicu Mismatch
Secara default, konfigurasi standar Jackson di Spring Boot dapat menghasilkan representasi data yang tidak simetris antara backend dan frontend runtime JavaScript.
1. Format java.time (Timestamp vs ISO-8601)
Secara default, tanpa modul waktu yang terkonfigurasi eksplisit, Jackson mengonversi tipe data java.time.Instant atau LocalDateTime menjadi numeric array (misal: [2026, 3, 30, 10, 15, 30]) atau epoch timestamp (angka milidetik). Ketika frontend mengeksekusi new Date(payload.createdAt).toISOString() atau format tanggal terlokalisasi, representasi numerik ini sering kali diinterpretasikan berbeda atau memicu parsing error di JavaScript.
2. Divergensi Zona Waktu Server-Klien
Jika server memformat tanggal menggunakan zona waktu lokal mesin host (misalnya WIB/UTC+7) tanpa penanda offset, dan browser klien mengeksekusi hidrasi di zona waktu berbeda (misalnya UTC), fungsi seperti Intl.DateTimeFormat atau library tanggal di frontend akan menghasilkan teks tanggal yang berbeda antara server markup dan client render.
3. Serialisasi Nilai Null dan Inklusi Properti
Penggunaan JsonInclude.Include.NON_NULL pada backend menghilangkan properti dengan nilai null dari JSON output. Di sisi JavaScript, ketiadaan key menghasilkan nilai undefined. Pola perbandingan seperti state.value === null menghasilkan nilai false di klien padahal di server bernilai true, memicu perbedaan percabangan render komponen.
Solusi: Konfigurasi Jackson2ObjectMapperBuilderCustomizer
Standarisasi Jackson untuk SSR membutuhkan output ISO-8601 berbasis UTC dan konsistensi representasi nilai null. Konfigurasikan bean Jackson2ObjectMapperBuilderCustomizer di Spring Boot.
package com.example.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.TimeZone;
@Configuration
public class JacksonSsrConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
// Registrasi modul Java 8 Date/Time
builder.modules(new JavaTimeModule());
// Nonaktifkan penulisan tanggal sebagai integer timestamp
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// Pastikan zona waktu backend selalu UTC untuk JSON API/State
builder.timeZone(TimeZone.getTimeZone("UTC"));
// Pertahankan properti null agar key tetap konsisten di frontend
builder.serializationInclusion(JsonInclude.Include.ALWAYS);
// Toleransi field baru di frontend tanpa memutus deserialisasi
builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
};
}
}Menyematkan State ke Tag Script Secara Aman
Kesalahan umum lainnya adalah mencetak string JSON langsung ke dalam tag <script> HTML tanpa sanitasi karakter penutup tag. Jika payload mengandung karakter string </script>, parser HTML browser akan memutus eksekusi script sebelum waktunya, merusak payload JSON dan menggagalkan hidrasi.
Implementasi Controller Spring Boot
package com.example.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.Instant;
@Controller
public class SsrViewController {
private final ObjectMapper objectMapper;
public SsrViewController(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public record PageState(String user, Instant renderedAt, String status) {}
@GetMapping("/ssr-page")
public String renderPage(Model model) throws JsonProcessingException {
PageState state = new PageState("developer", Instant.now(), null);
// Serialisasi objek ke string JSON
String rawJson = objectMapper.writeValueAsString(state);
// Escape karakter berbahaya untuk konteks HTML script block
String safeJson = rawJson
.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("&", "\\u0026");
model.addAttribute("initialState", safeJson);
return "index";
}
}Injeksi pada Template HTML (Thymeleaf)
Gunakan tag script khusus dengan tipe application/json untuk menghindari eksekusi script sembarang dan parsing yang aman:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SSR State Page</title>
</head>
<body>
<div id="app"><!-- Server-rendered markup here --></div>
<!-- Simpan state sebagai data block JSON aman -->
<script id="__INITIAL_STATE__" type="application/json" th:utext="${initialState}"></script>
<script src="/js/bundle.js"></script>
</body>
</html>Verifikasi dan Konsumsi di Frontend
Di sisi klien (React/Vue), ambil state mentah dari element script sebelum melakukan hidrasi.
// client-entry.js
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
function getInitialState() {
const stateElement = document.getElementById('__INITIAL_STATE__');
if (!stateElement || !stateElement.textContent) {
return null;
}
try {
return JSON.parse(stateElement.textContent);
} catch (err) {
console.error('Gagal parsing initial state:', err);
return null;
}
}
const initialState = getInitialState();
const container = document.getElementById('app');
hydrateRoot(container, <App initialState={initialState} />);Catatan Format Tanggal: Hindari pemformatan tanggal berbasis zona waktu lokal klien saat initial render jika server tidak merender komponen dengan zona waktu yang sama. Gunakan format ISO statis pada render pertama, lalu update format ke lokal klien di dalam hook
useEffect(React) atauonMounted(Vue).
Verifikasi Penghilangan Warning
Setelah konfigurasi di atas diterapkan, verifikasi status hidrasi melalui developer tools browser:
- Konsol browser bersih dari
Warning: Text content did not match. Server: ... Client: ...(React Error #418 / #423). - Vue Devtools tidak lagi mencatat
[Vue warn]: Hydration node mismatch. - Payload pada tag script
#__INITIAL_STATE__tervalidasi menghasilkan string ISO-8601 lengkap dengan penandaZ(contoh:"renderedAt":"2026-03-30T04:12:00Z") dan mempertahankan key dengan nilainull.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!