Saat membangun aplikasi menggunakan Inertia.js, masalah umum yang sering dihadapi adalah navigasi antar-halaman yang mereset scroll sidebar, mematikan audio player yang sedang berputar, mereset state formulir global, atau memicu peringatan Hydration node mismatch pada mode Server-Side Rendering (SSR). Masalah ini bersumber dari pola layout inline anonim yang memaksa virtual DOM (VDOM) melakukan unmount dan remount pada seluruh hierarki komponen.

Akar Masalah: Inline Layout vs Rekonsiliasi VDOM

Kesalahan umum adalah membungkus konten halaman langsung di dalam fungsi render komponen halaman:

// Bad Practice: Inline Layout Wrapper (React)
export default function Dashboard({ user }) {
  return (
    <AppLayout>
      <DashboardContent user={user} />
    </AppLayout>
  );
}

Ketika pengguna berpindah rute (misalnya dari /dashboard ke /settings), Inertia merender komponen baru. Karena AppLayout didefinisikan sebagai child di dalam fungsi render masing-masing halaman, VDOM (baik React maupun Vue) memperlakukan AppLayout baru sebagai node yang berbeda secara struktural.

Dampaknya adalah:

  • Unmount DOM Tree: Layout lama di-unmount total, menghapus seluruh internal state (seperti state useState, input pencarian, posisi scroll, atau koneksi WebSocket).
  • State Reset: Komponen layout baru di-mount dari awal, memicu siklus hidup (lifecycle hooks) ulang.
  • Hydration Mismatch pada SSR: Jika konfigurasi resolver komponen di sisi server (ssr.js) menghasilkan hierarki yang berbeda dengan sisi klien (app.js), engine hydration gagal mencocokkan DOM server dengan VDOM klien, menghasilkan rendering error dan penurunan performa rendering pertama kali.

Solusi: Mekanisme Persistent Layout

Inertia menyediakan mekanisme Persistent Layout. Mekanisme ini memisahkan hierarki layout dari siklus render halaman individual. Layout didefinisikan sebagai fungsi atau properti statis pada komponen halaman. Router Inertia akan mempertahankan instance layout yang sama di tingkat root dan hanya mengganti slot children saat navigasi berlangsung tanpa membongkar layout induk.

1. Implementasi pada React

Tetapkan layout melalui properti layout pada komponen halaman:

// Pages/Dashboard.jsx
import AppLayout from '@/Layouts/AppLayout';

export default function Dashboard({ user }) {
  return <DashboardContent user={user} />;
}

// Persistent Layout: Dieksekusi sekali di level router
Dashboard.layout = (page) => <AppLayout>{page}</AppLayout>;

Untuk menetapkan layout default secara otomatis ke semua halaman tanpa menuliskan properti berulang, modifikasi hook resolve di app.jsx:

// app.jsx
import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';
import AppLayout from '@/Layouts/AppLayout';

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
    const page = pages[`./Pages/${name}.jsx`];
    
    // Terapkan default layout jika halaman tidak mendefinisikannya secara eksplisit
    page.default.layout = page.default.layout || ((pageComponent) => <AppLayout>{pageComponent}</AppLayout>);
    
    return page;
  },
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />);
  },
});

2. Implementasi pada Vue 3

Pada Vue 3 menggunakan Single File Component (SFC) dengan <script setup>, gunakan defineOptions untuk menetapkan properti layout:

<!-- Pages/Dashboard.vue -->
<script setup>
import DashboardContent from '@/Components/DashboardContent.vue';
import AppLayout from '@/Layouts/AppLayout.vue';

defineOptions({
  layout: AppLayout,
});

defineProps({
  user: Object,
});
</script>

<template>
  <DashboardContent :user="user" />
</template>

Pengaturan layout global pada Vue 3 via app.js:

// app.js
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./Pages/**/*.vue', { eager: true });
    const page = pages[`./Pages/${name}.vue`];
    
    page.default.layout = page.default.layout || AppLayout;
    
    return page;
  },
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el);
  },
});

Menghindari Hydration Mismatch pada SSR

Hydration mismatch terjadi ketika HTML yang dihasilkan oleh server (Node.js runtime) berbeda dengan HTML yang dirender saat client hydration pertama kali. Dalam konteks Inertia persistent layout, mismatch sering dipicu oleh dua hal:

  1. Perbedaan Resolver SSR dan Client: Konfigurasi resolve di app.js (client) dan ssr.js (server) tidak identik. Jika app.js memberikan default layout namun ssr.js tidak mengimplementasikannya, server merender halaman tanpa layout sementara klien merender dengan layout.
  2. Kondisi Browser-Only di Layout: Mengakses objek seperti window, localStorage, atau manipulasi DOM langsung di dalam siklus setup layout sebelum komponen ter-mount di klien.

Solusi konfigurasi ssr.js yang sinkron:

// ssr.js
import { createInertiaApp } from '@inertiajs/react'; // atau @inertiajs/vue3
import createServer from '@inertiajs/react/server';
import ReactDOMServer from 'react-dom/server';
import AppLayout from '@/Layouts/AppLayout';

createServer((page) =>
  createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: (name) => {
      const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
      const resolvedPage = pages[`./Pages/${name}.jsx`];
      
      // Samakan logika fallback layout persis seperti app.jsx
      resolvedPage.default.layout =
        resolvedPage.default.layout || ((p) => <AppLayout>{p}</AppLayout>);
        
      return resolvedPage;
    },
    setup: ({ App, props }) => <App {...props} />,
  })
);

Verifikasi Preservasi State

Untuk memverifikasi bahwa persistent layout berfungsi tanpa memicu re-render penuh pada DOM root:

  1. Tambahkan elemen input teks sederhana atau counter di dalam AppLayout: <input type="text" placeholder="Layout state test" />.
  2. Jalankan aplikasi di browser, ketikkan karakter ke dalam input tersebut.
  3. Lakukan navigasi antar halaman menggunakan <Link href="..."> atau router.visit().
  4. Jika teks pada input tetap ada dan fokus input tidak hilang saat halaman berpindah rute, persistent layout telah terpasang dengan benar.
Gunakan properti layout persisten pada level deklarasi komponen untuk menjamin integritas memori VDOM dan meniadakan beban re-mounting DOM nodes saat navigasi rute Inertia.