Laravel SSR sering memunculkan bug UI yang membingungkan ketika HTML dari server berbeda dengan hasil render pertama di browser. Gejalanya bisa berupa warning hydration mismatch, tombol login yang sebentar muncul lalu berubah, tema gelap/terang berkedip, bahasa berganti setelah halaman siap, atau komponen tertentu di-recreate ulang setelah hydration.
Masalah utamanya hampir selalu sama: state awal yang dipakai saat render di server tidak identik dengan state awal yang dipakai di browser. Di aplikasi Laravel yang memakai Inertia.js atau integrasi frontend SSR lain, sumber mismatch paling umum adalah session, cookie, auth user, locale, timezone, dan A/B flag. Solusinya bukan sekadar menyembunyikan warning, tetapi memastikan sumber state awal benar-benar deterministik, aman diserialisasi, dan konsisten di kedua sisi.
Kenapa hydration mismatch terjadi pada Laravel SSR
Pada SSR, server mengirim HTML yang sudah dirender berdasarkan data tertentu. Setelah itu, browser menjalankan JavaScript dan melakukan hydration, yaitu menghubungkan event handler dan state ke HTML yang sudah ada. Jika output render pertama di browser tidak sama dengan HTML dari server, framework frontend akan mendeteksi perbedaan.
Dalam konteks Laravel, mismatch biasanya muncul ketika nilai berikut dibaca dengan cara yang berbeda:
- Session: misalnya flash message tersedia di server, tetapi sudah berubah saat browser melakukan request lain atau state lokal mengambil nilai berbeda.
- Cookie: tema, eksperimen A/B, atau preferensi UI dibaca dari cookie di browser, tetapi tidak ikut dijadikan props SSR.
- Auth: server tahu user sudah login, tetapi frontend menghitung state auth dari sumber lain saat boot.
- Locale: server merender bahasa Indonesia, browser mengubah ke bahasa lain berdasarkan local storage atau
navigator.language. - Timezone: server memformat tanggal dalam satu zona waktu, browser memformat ulang dengan zona waktu lokal user.
- Browser-only API: komponen membaca
window,document,localStorage, atau ukuran viewport saat render awal.
Prinsip dasarnya sederhana: render pertama di browser harus menghasilkan markup yang sama dengan render di server.
Gejala yang sering terlihat
Warning hydration di console
Framework frontend biasanya menampilkan warning bahwa text content, atribut, atau struktur node tidak cocok dengan hasil SSR.
UI berkedip atau berubah setelah load
Contoh paling umum:
- Status login berubah dari Masuk ke Dashboard sesaat setelah halaman siap.
- Tema terang dirender di server lalu langsung berubah ke gelap di browser.
- Bahasa default server muncul sesaat lalu diganti oleh preferensi dari client.
- Komponen tanggal berubah format setelah hydration.
Event handler hilang atau komponen di-render ulang
Pada mismatch yang lebih berat, framework bisa membuang subtree tertentu dan merender ulang di client. Hasilnya bukan hanya warning, tetapi juga performa lebih buruk dan state lokal komponen bisa hilang.
Akar masalah: state awal tidak deterministik
Penyebab paling penting untuk dipahami adalah state awal dihitung dari dua sumber yang berbeda. Server merender berdasarkan request Laravel. Browser merender ulang berdasarkan API browser, cookie lokal, local storage, atau hitungan waktu saat itu. Kalau sumbernya tidak sama, hasilnya juga tidak sama.
Ini contoh anti-pattern yang sering terjadi:
- Server merender komponen dari props Inertia, tetapi browser menghitung ulang nilai awal dari
document.cookie. - Server memakai locale dari middleware Laravel, tetapi frontend langsung membaca locale dari local storage saat render pertama.
- Server memformat tanggal menjadi string lokal, sementara browser memanggil
toLocaleString()sendiri saat render. - Server tidak mengirim feature flag ke page props, lalu client memutuskan varian sendiri berdasarkan cookie.
Jika Anda ingin SSR stabil, pilih satu sumber kebenaran untuk initial render: biasanya props SSR yang dibentuk dari request Laravel.
Contoh mismatch yang realistis
1. Tema dari cookie dibaca berbeda di server dan client
Misalnya Anda menyimpan tema di cookie theme=dark. Server tidak mengirim nilai ini ke props SSR, tetapi komponen frontend membaca cookie langsung saat render pertama.
// Anti-pattern: nilai awal ditentukan dari browser saat render pertama
const theme = getCookie('theme') || 'light'
return <body className={theme === 'dark' ? 'dark' : ''}>...</body>Kalau server merender mode terang, tetapi browser menemukan cookie mode gelap, maka class awal berbeda dan hydration mismatch terjadi.
2. Auth state dihitung ulang di client
Server sudah tahu user login lewat guard Laravel. Namun di frontend, komponen navbar malah menentukan status login dari token atau state store yang belum sinkron.
// Anti-pattern
const isLoggedIn = !!localStorage.getItem('token')Jika SSR merender menu untuk user login, tetapi browser belum menemukan token atau sebaliknya, struktur HTML navbar akan berbeda.
3. Locale dan timezone memengaruhi format tanggal
Ini kasus yang sangat sering terlewat. Server merender:
<span>10 Juni 2026, 08.00</span>Tetapi browser merender ulang menggunakan locale/timezone lokal user:
new Date(value).toLocaleString()Hasilnya bisa berubah menjadi format berbeda, bahkan hari dan tanggal bisa bergeser jika zona waktu berbeda.
Cara reproduksi masalah di Laravel + Inertia.js
Berikut contoh sederhana menggunakan shared props Inertia. Tujuannya bukan menunjukkan setup penuh, tetapi memperlihatkan bagaimana mismatch mudah terjadi.
Shared props dari Laravel
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user()
? [
'id' => $request->user()->id,
'name' => $request->user()->name,
]
: null,
],
'preferences' => [
'theme' => $request->cookie('theme', 'light'),
'locale' => app()->getLocale(),
],
'flash' => [
'message' => $request->session()->get('message'),
],
]);
}
}Di sini server sudah punya sumber data yang benar dari request saat itu. Masalah muncul jika komponen frontend mengabaikan props ini dan menghitung ulang state awal dari browser.
Komponen frontend yang rentan mismatch
import { usePage } from '@inertiajs/vue3'
export default {
computed: {
authUser() {
return this.$page.props.auth.user
},
initialTheme() {
// Anti-pattern: mengabaikan props SSR dan membaca browser langsung
return document.cookie.includes('theme=dark') ? 'dark' : 'light'
}
}
}Jika server merender berdasarkan $page.props.preferences.theme tetapi komponen memakai document.cookie saat render awal, mismatch sangat mungkin terjadi.
Langkah perbaikan: buat initial state deterministik
1. Jadikan request Laravel sebagai sumber kebenaran untuk SSR
Nilai yang memengaruhi tampilan awal harus dibaca di server dan dikirim sebagai props SSR. Untuk Laravel + Inertia, tempat yang paling umum adalah middleware shared props atau saat membentuk response halaman.
Yang penting bukan banyaknya props, tetapi konsistensi. Jika tema, locale, auth, dan flag memengaruhi HTML awal, semuanya harus berasal dari request yang sama.
2. Gunakan props yang sama saat render pertama di browser
Di komponen frontend, render pertama harus memakai props yang sudah disuntikkan dari SSR, bukan menghitung ulang dari browser-only source.
import { usePage } from '@inertiajs/vue3'
import { computed, onMounted, ref } from 'vue'
export default {
setup() {
const page = usePage()
const theme = ref(page.props.preferences.theme)
onMounted(() => {
// Setelah hydration selesai, sinkronisasi browser-only behavior jika perlu
document.documentElement.dataset.theme = theme.value
})
return { theme }
}
}Pola ini bekerja karena nilai awal di browser sama dengan nilai yang dipakai server saat membentuk HTML.
3. Sinkronisasi perubahan browser-only setelah mounted
Ada data yang memang hanya tersedia di browser, seperti ukuran viewport, preferensi sistem, atau local storage. Jangan pakai data ini untuk menentukan HTML awal SSR jika nilainya tidak tersedia di server.
Gunakan fallback deterministik saat SSR, lalu ubah setelah komponen mounted.
const mounted = ref(false)
const viewport = ref('desktop')
onMounted(() => {
mounted.value = true
viewport.value = window.innerWidth < 768 ? 'mobile' : 'desktop'
})Lalu render UI yang aman saat belum mounted, misalnya skeleton ringan atau layout netral yang tidak menyebabkan struktur berbeda drastis.
Pola aman untuk session, cookie, auth, locale, timezone, dan flag
Session dan flash message
Flash message dari session aman dipakai jika dikirim langsung dari Laravel ke props halaman. Hindari mengambil flash state dari store frontend yang tidak berasal dari page props awal.
Jika pesan bisa hilang setelah navigasi berikutnya, itu bukan masalah hydration selama render pertama browser masih memakai nilai yang sama dengan SSR.
Cookie
Cookie sering jadi penyebab utama karena mudah dibaca langsung di browser. Untuk SSR, bacalah cookie lewat request Laravel dan kirim nilainya ke frontend sebagai initial props. Jika browser perlu memperbarui cookie setelah interaksi user, lakukan setelah hydration dan sinkronkan ke request berikutnya.
Auth
Gunakan user dari Laravel guard sebagai state awal. Jangan tentukan auth state dari token di local storage untuk markup awal. Kalau memang ada mekanisme token client-side terpisah, tampilkan fallback netral sampai status final diketahui, bukan memaksa render kondisi yang belum pasti.
Locale
Tentukan locale di middleware Laravel atau dari request, lalu kirim ke props. Jangan langsung mengganti locale saat render pertama dari local storage. Jika Anda tetap ingin menyimpan preferensi bahasa di client, sinkronkan ke server lebih dulu atau terapkan setelah hydration dengan sadar bahwa tampilan awal harus tetap konsisten.
Timezone
Ini salah satu kasus paling sulit jika Anda memformat tanggal langsung saat render. Pendekatan paling aman:
- Kirim timestamp mentah atau string ISO dari server.
- Untuk SSR, tampilkan format netral atau format server yang konsisten.
- Jika ingin format sesuai timezone user, lakukan setelah mounted atau setelah timezone client diketahui dan disimpan.
Jangan berharap server bisa menebak timezone browser secara akurat pada request pertama tanpa mekanisme sinkronisasi sebelumnya.
A/B flag atau feature flag
Jika flag menentukan cabang UI yang berbeda, server dan client harus memakai nilai yang sama. Jangan menghitung varian eksperimen secara acak di frontend saat render awal. Persistensikan flag di cookie atau backend, lalu bagikan ke SSR props.
Serialisasi data yang aman dan stabil
State awal SSR biasanya diserialisasi ke HTML lalu dipakai browser saat hydration. Karena itu, data yang Anda kirim harus:
- Minimal: kirim hanya yang dibutuhkan komponen awal.
- Aman: jangan serialisasi model penuh, token rahasia, atau data sensitif.
- Stabil: hindari object kompleks yang sulit diprediksi bentuknya.
- Konsisten: gunakan tipe data yang sama di server dan client.
Untuk auth user, kirim field yang memang dibutuhkan UI, bukan seluruh objek user. Untuk tanggal, kirim string yang jelas. Untuk flag, kirim boolean eksplisit, bukan string samar seperti '0' atau 'false' yang mudah salah ditafsirkan di JavaScript.
// Lebih aman dan stabil
{
auth: {
user: user ? { id: user.id, name: user.name } : null
},
preferences: {
theme: 'dark',
locale: 'id'
},
flags: {
newNavbar: true
}
}Guard untuk kode browser-only
Semua akses ke API browser harus diperlakukan hati-hati dalam SSR. Selain mencegah error runtime, guard ini juga mencegah render awal memakai nilai yang berbeda dari server.
// Contoh umum
const isBrowser = typeof window !== 'undefined'
let storedTheme = null
if (isBrowser) {
storedTheme = window.localStorage.getItem('theme')
}Namun guard saja tidak cukup. Kesalahan yang sering terjadi adalah:
"Kalau ada window, baca local storage saat setup agar lebih akurat."
Ini tetap bisa menyebabkan mismatch, karena pada render pertama di browser nilainya berbeda dari SSR. Guard mencegah error, tetapi belum tentu mencegah hydration mismatch. Jika nilai browser-only memengaruhi markup awal, tunda pemakaiannya sampai mounted.
Fallback loading yang benar
Jika sebagian state memang belum bisa diketahui secara pasti saat SSR, lebih baik tampilkan fallback yang netral dan konsisten daripada merender UI final yang berpotensi berbeda.
Contoh yang masuk akal:
- Tampilkan placeholder profil sampai status auth final selesai disinkronkan.
- Tampilkan format tanggal generik atau skeleton sebelum timezone client diterapkan.
- Gunakan class tema dari SSR, lalu sinkronkan preferensi sistem hanya setelah mounted.
Trade-off-nya adalah ada sedikit keterlambatan sebelum UI final tampil. Tetapi ini lebih aman daripada mismatch yang membuat subtree dirender ulang dan menimbulkan flicker.
Anti-pattern yang sebaiknya dihindari
- Membaca cookie, local storage, atau navigator langsung saat render pertama untuk menentukan cabang UI SSR.
- Mencampur dua sumber kebenaran, misalnya auth dari Laravel di server tetapi auth dari token lokal di browser.
- Memformat tanggal lokal langsung di template tanpa strategi timezone yang jelas.
- Mengandalkan nilai acak seperti
Math.random()atau timestamp saat render untuk id, key, atau varian UI. - Mengirim model atau state berlebihan ke props SSR yang sulit dijaga konsistensinya.
- Mengubah locale atau theme terlalu dini sebelum hydration selesai.
Checklist debugging hydration mismatch di Laravel SSR
1. Bandingkan HTML server dengan render pertama client
Lihat warning console, lalu fokus pada node pertama yang berbeda. Biasanya masalah bukan di komponen besar, tetapi pada satu text node, class, atau conditional branch.
2. Audit semua nilai yang memengaruhi markup awal
Tanyakan untuk setiap nilai: asalnya dari mana saat SSR, dan asalnya dari mana saat browser render pertama? Jika jawabannya berbeda, itu kandidat utama.
3. Periksa shared props Inertia
Pastikan auth, locale, theme, flash, dan flags benar-benar ikut dibagikan dari request Laravel. Verifikasi juga nilainya sesuai request saat itu.
4. Cari penggunaan browser-only API di setup/render path
Audit komponen untuk window, document, localStorage, navigator, ukuran layar, dan waktu lokal. Kalau dipakai sebelum mounted, periksa ulang.
5. Cek format tanggal dan angka
Perbedaan locale/timezone sering menghasilkan mismatch yang tidak langsung terlihat. Jika ada komponen tanggal, curigai itu lebih dulu.
6. Pastikan tipe data konsisten
Boolean vs string boolean, null vs object kosong, atau array kosong vs undefined bisa mengubah percabangan render.
7. Nonaktifkan logika turunan satu per satu
Untuk menemukan akar masalah, sederhanakan komponen sampai mismatch hilang. Setelah itu aktifkan lagi bertahap: auth, theme, locale, tanggal, flags, dan seterusnya.
Contoh pola implementasi yang lebih aman
Berikut contoh pendekatan praktis: semua state yang memengaruhi tampilan awal dikirim dari Laravel, lalu browser hanya melakukan sinkronisasi lanjutan setelah mounted.
Laravel: shared initial state
<?php
public function share(Request $request): array
{
$user = $request->user();
return [
'auth' => [
'user' => $user ? [
'id' => $user->id,
'name' => $user->name,
] : null,
],
'ui' => [
'theme' => $request->cookie('theme', 'light'),
'locale' => app()->getLocale(),
'flags' => [
'newNavbar' => (bool) $request->cookie('ab_new_navbar', false),
],
],
];
}Frontend: gunakan props untuk initial render
import { usePage } from '@inertiajs/vue3'
import { computed, onMounted } from 'vue'
export default {
setup() {
const page = usePage()
const user = computed(() => page.props.auth.user)
const theme = computed(() => page.props.ui.theme)
const newNavbar = computed(() => page.props.ui.flags.newNavbar)
onMounted(() => {
document.documentElement.dataset.theme = theme.value
})
return { user, theme, newNavbar }
}
}Dengan pola ini, navbar, tema, dan state auth pada render pertama berasal dari data yang sama antara server dan browser.
Kapan perlu memilih fallback dibanding render final
Tidak semua data layak dipaksakan sinkron pada request pertama. Jika nilainya memang hanya tersedia di browser dan mengubah markup secara signifikan, gunakan fallback. Contohnya timezone lokal yang akurat, preferensi sistem, atau data client-side yang baru tersedia setelah script berjalan.
Pilih render final sejak SSR jika:
- Data tersedia dari request Laravel dengan andal.
- Nilai memengaruhi struktur UI penting.
- Anda ingin menghindari flicker dan mismatch.
Pilih fallback jika:
- Data hanya tersedia di browser.
- Biaya sinkronisasi awal terlalu rumit.
- Perubahan setelah mounted masih bisa diterima dari sisi UX.
Penutup
Bug hydration pada Laravel SSR hampir selalu berakar pada satu hal: server dan browser tidak memulai dari state yang sama. Pada aplikasi Laravel dengan Inertia.js, cara paling aman adalah menjadikan request Laravel sebagai sumber kebenaran untuk state awal, membagikan data yang relevan lewat props SSR, menunda logika browser-only sampai mounted, dan memakai fallback saat nilai memang belum bisa diketahui secara deterministik.
Jika Anda menemukan UI berkedip, warning hydration, atau komponen berubah setelah load, jangan langsung menyalahkan framework. Audit dulu auth, cookie, session, locale, timezone, dan feature flag. Dalam banyak kasus, perbaikannya bukan rumit: cukup pastikan initial state konsisten, aman diserialisasi, dan tidak dihitung ulang dari sumber yang berbeda saat browser melakukan hydration.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!