Laravel SSR: debug hydration state bocor pada komponen interaktif biasanya muncul saat HTML hasil render server tidak identik dengan state awal yang dipakai di browser. Gejalanya sering terlihat sebagai warning hydration mismatch, nilai default yang berubah setelah mount, flash message menghilang, input ter-reset, atau tampilan sempat benar lalu meloncat.
Pada aplikasi Laravel yang memakai SSR untuk frontend interaktif, akar masalahnya hampir selalu sama: state awal tidak dibentuk secara deterministik dan tidak diserialisasi konsisten. Selain itu, bug juga sering datang dari pemakaian waktu atau nilai acak saat render, akses API browser ketika kode masih berjalan di server, atau state global yang hidup lebih lama dari satu request lalu bocor ke request lain.
Memahami konteks bug hydration di Laravel SSR
Pada pola SSR, server mengembalikan HTML awal agar halaman cepat tampil dan ramah SEO. Setelah itu, JavaScript di browser melakukan hydrate, yaitu memasang event handler dan menyambungkan komponen interaktif ke HTML yang sudah ada.
Hydration hanya aman jika dua hal berikut identik:
- HTML yang dihasilkan saat SSR.
- State awal yang dipakai komponen saat dijalankan di browser.
Kalau salah satu berbeda, framework frontend akan mencoba memperbaiki DOM atau me-render ulang sebagian komponen. Dari sinilah muncul gejala seperti:
- HTML awal berbeda dengan state klien, sehingga muncul warning mismatch.
- Nilai default berubah setelah mount, misalnya tanggal, locale, atau user preference berbeda.
- Flash message hilang, karena hanya tersedia di request server dan tidak ikut dibawa ke state klien.
- Input ter-reset, karena nilai server tidak sama dengan model state di browser.
- UI meloncat, misalnya skeleton, badge, menu, atau counter berubah tepat setelah hydration.
Di ekosistem Laravel, pola ini umum terjadi pada kombinasi Laravel dengan SSR frontend seperti Inertia SSR, Vue SSR, React SSR, atau integrasi kustom yang menyuntikkan page props dari server ke halaman.
Akar masalah yang paling sering
1. Data request-spesifik tidak diserialisasi secara konsisten
Request-spesifik artinya data yang hanya valid untuk satu request, misalnya:
- flash message dari session,
- user login saat ini,
- locale aktif,
- CSRF token,
- hasil validasi form,
- query string yang mempengaruhi filter.
Bug terjadi ketika server memakai data itu saat render HTML, tetapi browser tidak menerima nilai awal yang sama. Hasilnya, komponen di klien membangun state berbeda dari HTML yang sudah ada.
Contoh buruk: server menampilkan banner sukses dari session, tetapi props yang dikirim ke klien tidak memuat flash message yang sama.
// Contoh sebelum: flash dipakai di server, tapi tidak ikut ke payload klien secara konsisten<?php
return view('app', [
'page' => $page,
'flashSuccess' => session('success'),
]);Jika komponen frontend membaca flash dari sumber lain atau tidak mendapatkannya sama sekali saat hydrate, banner bisa hilang setelah mount.
Pendekatan yang lebih aman adalah memastikan semua data yang dipakai untuk render awal masuk ke payload state yang sama, bukan sebagian di view Blade dan sebagian lagi dihitung ulang di browser.
// Contoh sesudah: semua state awal yang dipakai komponen disatukan ke payload
<?php
return view('app', [
'page' => [
...$page,
'props' => [
...($page['props'] ?? []),
'flash' => [
'success' => session('success'),
'error' => session('error'),
],
'auth' => [
'user' => optional(request()->user())->only(['id', 'name', 'email']),
],
'locale' => app()->getLocale(),
],
],
]);Prinsip penting: jika sebuah nilai mempengaruhi HTML awal, nilai itu harus tersedia dalam payload hydration yang sama dan dalam bentuk yang stabil.
2. Menggunakan waktu atau nilai acak saat render awal
Pemanggilan seperti now(), date(), Str::uuid(), rand(), atau generator ID acak di dalam jalur render hampir selalu berisiko. Server dan browser menjalankan render pada waktu berbeda, sehingga output bisa berubah beberapa milidetik kemudian.
Contoh buruk:
<script setup>
const generatedAt = new Date().toLocaleTimeString()
const tempId = Math.random().toString(36).slice(2)
</script>
<template>
<div :id="tempId">Dirender pada {{ generatedAt }}</div>
</template>Saat SSR, HTML dibentuk dengan satu nilai. Saat hydrate di browser, nilainya berbeda. Akibatnya teks dan atribut tidak cocok.
Perbaikan yang lebih aman:
- Hitung nilainya sekali di server, lalu kirim sebagai props.
- Atau, kalau nilai memang hanya relevan di browser, isi setelah mount dan jangan jadikan bagian dari HTML SSR yang harus cocok.
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
generatedAt: String,
})
const clientOnlyId = ref(null)
onMounted(() => {
clientOnlyId.value = crypto.randomUUID()
})
</script>
<template>
<div>
<span>Dirender pada {{ props.generatedAt }}</span>
<span v-if="clientOnlyId">ID sesi: {{ clientOnlyId }}</span>
</div>
</template>Dengan cara ini, bagian yang harus match saat hydration tetap stabil, sementara data yang benar-benar client-only muncul setelah mount.
3. Mengakses browser API saat SSR
Pemakaian window, document, localStorage, sessionStorage, navigator, atau ukuran viewport pada render awal sering memicu mismatch. Di server, API itu tidak tersedia atau fallback-nya berbeda dari browser.
Contoh umum:
// Sebelum
const theme = localStorage.getItem('theme') || 'light'Di server, nilai ini tidak ada. SSR mungkin memakai light, tetapi browser menemukan dark sehingga markup awal berubah saat hydrate.
Pendekatan yang lebih aman:
- Jika tema harus mempengaruhi HTML awal, ambil dari cookie/request di Laravel dan kirim sebagai props.
- Jika hanya enhancement di browser, baca
localStoragesetelah mount.
// Sesudah: nilai awal berasal dari server, browser hanya sinkronisasi tambahan
const props = defineProps({ initialTheme: String })
const theme = ref(props.initialTheme)
onMounted(() => {
const saved = localStorage.getItem('theme')
if (saved && saved !== theme.value) {
theme.value = saved
}
})Di sisi Laravel, Anda bisa membaca cookie tema dari request agar server dan klien memulai dari nilai yang sama.
4. State global bocor antar request
Ini salah satu kasus paling berbahaya dalam Laravel SSR: ada singleton, cache in-memory, variabel statis, atau store global yang menyimpan data request sebelumnya lalu dipakai ulang saat SSR berikutnya. Gejalanya aneh karena tidak selalu muncul di lokal, tetapi sering terlihat di environment yang proses SSR-nya hidup lama.
Contoh pola berbahaya:
<?php
class SsrContext
{
public static array $shared = [];
}
// Request A
SsrContext::$shared['flash'] = session('success');
// Request B bisa tidak sengaja membaca nilai lama jika tidak di-resetAtau store frontend global dibuat sekali di level modul dan dipakai ulang untuk semua request SSR. Pada runtime SSR yang persisten, state seperti ini dapat bocor antar user.
Perbaikan yang benar adalah membuat state baru untuk setiap request, lalu memastikan konteks request dibersihkan setelah selesai dipakai.
// Pseudocode: buat store/context baru per request
function renderPage($request, $page)
{
$store = createStore([
'auth' => [
'user' => optional($request->user())->only(['id', 'name', 'email']),
],
'flash' => [
'success' => $request->session()->get('success'),
],
]);
return ssrRender($page, $store);
}Jika Anda memakai service container Laravel, hindari menyimpan data request-spesifik pada singleton yang hidup lintas request. Simpan data tersebut di object yang memang dibuat per request, atau operkan sebagai parameter eksplisit ke proses render.
Alur debugging langkah demi langkah
1. Konfirmasi bahwa ini memang hydration mismatch
Mulai dari gejala yang bisa diamati:
- Apakah ada warning hydration di console browser?
- Apakah elemen yang berubah persis setelah JavaScript aktif?
- Apakah HTML awal dari server berbeda dengan hasil inspect setelah mount?
Jika UI hanya berubah setelah request API kedua, itu belum tentu hydration. Fokus pada perubahan yang terjadi tepat saat aplikasi frontend mengambil alih DOM SSR.
2. Bekukan area yang bermasalah
Isolasi komponen terkecil yang menimbulkan gejala. Jangan debug satu halaman penuh sekaligus. Cari area seperti:
- banner flash,
- komponen form,
- navbar user login,
- counter/cart badge,
- filter berbasis query string,
- komponen tanggal atau relative time.
Jika perlu, matikan sementara komponen interaktif lain untuk memastikan sumber mismatch hanya satu.
3. Bandingkan payload server dengan state awal klien
Pada Laravel SSR, ini langkah paling penting. Ambil payload yang dikirim server ke halaman, lalu bandingkan dengan nilai yang dipakai komponen saat inisialisasi di browser.
Checklist pemeriksaan:
- Apakah semua props yang dipakai untuk render awal benar-benar ada di payload?
- Apakah ada tipe data yang berubah, misalnya integer menjadi string, object menjadi array, atau
nullmenjadi nilai default lain? - Apakah ada field yang dihitung ulang di browser padahal seharusnya memakai nilai dari server?
Bug sering terjadi bukan karena field hilang total, melainkan karena bentuk serialisasinya berubah. Contoh: tanggal di server dikirim sebagai string ISO, tetapi di klien langsung diformat ulang dengan timezone lokal saat render pertama.
4. Cari sumber nondeterministik
Audit komponen yang bermasalah dan cari hal-hal berikut:
now(),new Date(), relative time, countdown.Math.random(), UUID acak, key dinamis.- akses
window,document,localStorage. - pembacaan ukuran layar, preferensi browser, atau timezone browser.
Jika salah satu ditemukan di jalur render awal, pindahkan ke:
- props dari server, atau
- hook setelah mount jika memang client-only.
5. Audit state global dan singleton
Periksa dua sisi sekaligus:
- Laravel: singleton, service provider, helper statis, cache in-memory proses, variabel global.
- SSR runtime/frontend: store global level modul, state yang dideklarasikan di luar factory fungsi, cache yang tidak di-scope per request.
Jika bug hanya muncul saat trafik ramai, saat user berganti akun pada browser berbeda, atau pada worker/proses yang hidup lama, kebocoran state lintas request sangat mungkin menjadi penyebab.
6. Log nilai tepat sebelum render server dan tepat sebelum hydrate klien
Tambahkan logging minimal namun terarah. Jangan log seluruh payload besar kalau tidak perlu; pilih field yang mencurigakan.
// Server-side pseudo logging
logger()->debug('SSR props', [
'flash' => $page['props']['flash'] ?? null,
'user_id' => $page['props']['auth']['user']['id'] ?? null,
'locale' => $page['props']['locale'] ?? null,
]);// Client-side pseudo logging
console.debug('Hydration props', {
flash: page.props.flash,
userId: page.props.auth?.user?.id,
locale: page.props.locale,
})Jika nilainya berbeda, Anda sudah mempersempit masalah dari sisi serialisasi atau inisialisasi state.
7. Uji request berurutan untuk mendeteksi kebocoran
Untuk kasus state global bocor, lakukan request berurutan dengan konteks berbeda:
- User A login dan membuka halaman dengan flash tertentu.
- User B membuka halaman yang sama dari sesi berbeda.
- Bandingkan apakah HTML SSR milik B pernah menampilkan state milik A.
Ini juga bisa diuji dengan skrip sederhana atau test end-to-end yang memukul endpoint SSR dengan cookie/session berbeda.
Checklist isolasi masalah
Gunakan checklist ini saat debug bug hydration di Laravel SSR:
- Apakah komponen memakai data request-spesifik yang tidak ikut ke props awal?
- Apakah ada nilai waktu, random, UUID, atau relative time saat render awal?
- Apakah ada akses browser API sebelum mount?
- Apakah state awal di browser dibuat ulang, bukan memakai payload server apa adanya?
- Apakah ada singleton atau variabel statis yang menyimpan state per user?
- Apakah store SSR dibuat baru untuk setiap request?
- Apakah data yang sama diformat berbeda di server dan browser, misalnya timezone atau locale?
- Apakah key list/komponen stabil, bukan hasil random?
- Apakah flash message dibaca sekali lalu terhapus sebelum sempat masuk payload klien?
Contoh kasus nyata: flash message hilang setelah hydration
Sebelum
Server menampilkan flash lewat Blade atau shared view, tetapi komponen frontend membaca state dari props yang tidak memuat flash.
<!-- Blade SSR shell -->
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endifSetelah hydrate, komponen interaktif mengambil alih area tersebut dan tidak menemukan data flash yang sama. Banner pun hilang.
Sesudah
Pindahkan sumber kebenaran ke payload bersama yang dipakai server dan klien.
<?php
$props = [
'flash' => [
'success' => session('success'),
],
];
return view('app', compact('props'));<template>
<div v-if="props.flash?.success" class="alert alert-success">
{{ props.flash.success }}
</div>
</template>Mengapa ini bekerja? Karena HTML SSR dan state awal klien berasal dari data yang sama, bukan dua sumber berbeda.
Contoh kasus nyata: input ter-reset setelah mount
Akar masalah
Server merender value awal dari old input atau query string, tetapi komponen klien menginisialisasi model dengan default lain.
// Sebelum
const form = reactive({
search: '',
})Di server, input mungkin tampil berisi ?search=laravel. Namun saat hydrate, state klien bernilai string kosong, sehingga input ter-reset.
Perbaikan
// Sesudah
const props = defineProps({
filters: Object,
})
const form = reactive({
search: props.filters?.search ?? '',
})Pastikan filter yang membentuk UI awal selalu datang dari props server, bukan dibaca ulang dengan logika lain di klien.
Contoh kasus nyata: UI meloncat karena timezone berbeda
Tanggal sering tampak benar saat SSR lalu berubah saat hydrate karena server dan browser memformat ke timezone berbeda.
// Sebelum
const label = new Intl.DateTimeFormat().format(new Date(props.createdAt))Jika label ini dipakai langsung dalam render awal, output browser bisa berbeda dari server.
Opsi perbaikan:
- Kirim string yang sudah diformat final dari server jika konsistensi lebih penting.
- Atau render placeholder stabil saat SSR, lalu format lokal setelah mount jika memang harus mengikuti timezone user.
Trade-off-nya jelas: formatting di server memberi output konsisten, tetapi mungkin tidak sesuai timezone browser. Formatting di klien lebih personal, tetapi harus diperlakukan sebagai client-only agar tidak menabrak hydration.
Strategi pencegahan
1. Gunakan satu sumber kebenaran untuk state awal
Semua data yang membentuk HTML awal harus berasal dari payload yang sama. Hindari kondisi di mana Blade, middleware, dan komponen frontend masing-masing menghitung state sendiri.
2. Bedakan data SSR-safe dan client-only
Data SSR-safe bersifat deterministik dan tersedia pada request server. Data client-only bergantung pada browser, perangkat, viewport, storage lokal, atau waktu berjalan. Jangan campur keduanya dalam render awal.
3. Buat store baru per request SSR
Jangan gunakan store global yang diinisialisasi sekali lalu dipakai terus. Untuk SSR, state harus dibuat melalui factory agar setiap request mendapat instance baru.
4. Hindari singleton untuk data per user
Di Laravel, singleton cocok untuk service stateless atau utilitas yang aman dibagi. Untuk data auth, flash, locale, atau request context, jangan simpan pada object yang berumur lintas request.
5. Serialisasikan data secara eksplisit
Jika sebuah field penting, kirim dalam bentuk yang eksplisit dan stabil: string, boolean, integer, atau array sederhana. Jangan bergantung pada konversi implisit yang bisa berbeda antara server dan klien.
6. Tulis test untuk request berbeda
Selain menguji halaman untuk satu user, tambahkan skenario dua request berbeda untuk mendeteksi kebocoran state. Ini sangat berguna jika SSR berjalan pada proses persisten.
Pola pikir yang membantu saat memperbaiki hydration
Jangan langsung menganggap masalah ada di framework SSR. Pada banyak kasus Laravel SSR, bug berasal dari keputusan arsitektur state:
- state awal dihitung di dua tempat,
- request context tidak ikut diserialisasi,
- browser-only logic masuk ke render awal,
- atau ada shared state yang tidak di-reset per request.
Jika Anda memaksa HTML SSR dan state klien dibentuk dari input yang sama, deterministik, dan terisolasi per request, sebagian besar bug hydration akan hilang.
Ringkasan praktis
Untuk Laravel SSR: debug hydration state bocor pada komponen interaktif, fokuskan investigasi pada empat sumber utama: data request-spesifik yang tidak terserialisasi konsisten, penggunaan waktu atau random saat render awal, akses browser API pada SSR, dan state global yang bocor antar request.
Urutan kerja yang paling efektif adalah:
- identifikasi komponen yang berubah saat hydrate,
- bandingkan HTML/server payload dengan state awal klien,
- hapus sumber nondeterministik,
- pastikan context dibuat ulang per request,
- lalu uji dua request berbeda untuk memastikan tidak ada kebocoran state.
Kalau gejalanya adalah flash hilang, input ter-reset, default berubah setelah mount, atau UI meloncat, hampir selalu ada state awal yang tidak sinkron. Perbaikan yang benar bukan sekadar menyembunyikan warning, tetapi memastikan render server dan hydrate klien benar-benar memulai dari data yang sama.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!