Akar Masalah: Tabrakan SSR Props dan window.history.state
Inertia.js menyediakan hook useRemember untuk mempertahankan form state di dalam window.history.state saat pengguna berpindah halaman via navigasi browser (popstate). Masalah muncul ketika Server-Side Rendering (SSR) diaktifkan.
Siklus SSR merender markup HTML di server menggunakan props awal dari backend (misalnya nilai form kosong atau data awal database). Saat browser menerima markup ini, engine frontend (seperti React atau Vue) menjalankan proses hidrasi (hydration reconciliation). Jika useRemember membaca dan mengembalikan state dari window.history.state secara sinkron pada render pertama di sisi client, Virtual DOM client akan langsung berbeda dari DOM statis kiriman server.
Hasilnya adalah hydration drift: browser memunculkan peringatan hydration mismatch, dan input form mengalami flickering atau bahkan ter-reset paksa ke kondisi SSR awal.
Reproduksi Bug: Hydration Mismatch dan Flickering
Masalah ini terjadi ketika useRemember dieksekusi secara langsung pada level teratas komponen halaman yang dirender via SSR.
import { useRemember } from '@inertiajs/react';
export default function LeadEditForm({ lead }) {
// Bug: Di server nilainya adalah lead.email.
// Di client saat navigasi kembali, useRemember langsung mengambil cache dari history.state.
const [form, setForm] = useRemember({
email: lead.email,
notes: '',
}, 'LeadEditForm');
return (
<form>
<input
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<textarea
value={form.notes}
onChange={(e) => setForm({ ...form, notes: e.target.value })}
/>
</form>
);
}Console browser akan mencatat error rekonsiliasi:
Warning: Text content did not match. Server: "" Client: "[email protected]"
at textarea
at form
at LeadEditFormSolusi 1: Tunda Sinkronisasi ke Lifecycle Post-Mount
Hindari pembacaan state history secara langsung pada fase hidrasi pertama. Biarkan client merender DOM identik dengan server terlebih dahulu, lalu sinkronisasikan state dari history setelah komponen ter-mount sepenuhnya.
import { useState, useEffect } from 'react';
import { useRemember } from '@inertiajs/react';
export function useSafeRemember(initialState, key) {
// 1. Inisialisasi useRemember untuk sinkronisasi history
const [rememberedState, setRememberedState] = useRemember(initialState, key);
// 2. Gunakan local state yang selalu cocok dengan SSR pada first-pass render
const [state, setState] = useState(initialState);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
// Terapkan data dari cache history hanya setelah mount selesai
if (rememberedState !== initialState) {
setState(rememberedState);
}
}, []);
const updateState = (newState) => {
const resolved = typeof newState === 'function' ? newState(state) : newState;
setState(resolved);
setRememberedState(resolved);
};
return [state, updateState, isMounted];
}Penggunaan di komponen:
export default function LeadEditForm({ lead }) {
const [form, setForm, isHydrated] = useSafeRemember({
email: lead.email,
notes: '',
}, `LeadEditForm/${lead.id}`);
return (
<form>
<input
type="email"
value={form.email}
disabled={!isHydrated} // Cegah interaksi sebelum sinkronisasi tuntas
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<textarea
value={form.notes}
disabled={!isHydrated}
onChange={(e) => setForm({ ...form, notes: e.target.value })}
/>
</form>
);
}Solusi 2: Client-Only State Guard dan Serialisasi Bersih
Browser memberlakukan batasan kuota pada window.history.state (biasanya berkisar beberapa megabyte). Menyimpan seluruh objek form secara mentah tanpa pembersihan dapat merusak serialisasi popstate.
Gunakan Serialize Transform
Pastikan hanya field non-sensitif dan serializable yang masuk ke dalam memory history:
import { useEffect } from 'react';
import { useRemember } from '@inertiajs/react';
// ponytail: naive serializer, ceiling 64KB history state, upgrade ke IndexedDB jika form multi-step kompleks
const serializeForm = (data) => JSON.stringify({
email: data.email,
notes: data.notes,
});
const deserializeForm = (raw, fallback) => {
try {
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
};Konfigurasi preserveState pada Inertia Visits
Banyak developer salah mengombinasikan useRemember dengan opsi visit Inertia. Saat melakukan submit via form, gunakan opsi preserveState secara terukur agar history cache lokal tidak saling timpa dengan props baru hasil response server:
import { router } from '@inertiajs/react';
function submit(data) {
router.post('/leads', data, {
preserveState: (page) => Object.keys(page.props.errors).length > 0,
preserveScroll: true,
});
}Catatan: Jika response berhasil (redirect 303 tanpa validation errors), hindaripreserveState: trueabsolut. Hal ini bertujuan agar state form yang telah di-persist viauseRememberdi-reset ke nilai baru dan tidak memulihkan draft usang saat pengguna menekan tombol Back.
Checklist Verifikasi Navigasi Popstate
Jalankan prosedur validasi berikut untuk memastikan desinkronisasi form telah teratasi sepenuhnya:
- Hard Refresh (Ctrl+F5 / Cmd+Shift+R): Buka halaman dengan SSR aktif. Pastikan console bersih dari peringatan Hydration failed because the initial UI does not match what was rendered on the server.
- Navigasi Popstate (Browser Back/Forward): Isi sebagian input form, navigasi ke halaman lain via link internal Inertia, lalu tekan tombol Back browser. Form harus terisi kembali dengan draft terakhir tanpa layout flash.
- Bypass SSR Cache di Navigasi Client: Pastikan ID unik disertakan pada key
useRemember(misalnyaLeadEditForm/${lead.id}) agar draft entitas A tidak bocor ke entitas B. - Inspeksi history.state Payload: Buka DevTools Console dan jalankan
window.history.state. Pastikan ukuran payload data proporsional (< 50 KB) dan tidak mengandung objek sirkular atau referensi DOM node.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!