Posisi Inertia

Aplikasi bergaya SPA dengan Vue atau React tanpa membangun API terpisah.

Daftar isi

Inertia adalah lapisan penghubung antara Laravel dan frontend Vue atau React yang menghasilkan aplikasi bergaya single page application (SPA) tanpa membangun API JSON terpisah. Topik ini membahas model kerja protokolnya, langkah penyiapan minimum pada proyek Laravel, serta alur permintaan nyata dari kunjungan pertama sampai navigasi berikutnya. Seluruh keluaran diambil dari percobaan pada salinan proyek aplikasi catatan.

Posisi Inertia di antara Blade dan SPA#

Template Blade merender HTML lengkap pada setiap permintaan, sedangkan aplikasi SPA menyerahkan perenderan antarmuka sepenuhnya ke JavaScript di browser. Cara klasik menggabungkan keduanya adalah membangun backend API JSON yang berdiri sendiri beserta proyek SPA yang terpisah. Pendekatan itu memindahkan routing, validasi, dan otorisasi ke lapisan API, sehingga banyak aturan aplikasi ikut ditulis dua kali.

Inertia menghapus pemisahan tersebut tanpa menjadi framework baru. Paketnya terdiri atas dua adapter, yaitu penyesuai yang menghubungkan kedua sisi: inertiajs/inertia-laravel untuk server dan @inertiajs/vue3 atau @inertiajs/react untuk klien. Rute tetap berada pada routes/web.php, controller tetap menangani validasi dan otorisasi seperti biasa, tetapi responsnya berisi nama komponen halaman JavaScript beserta datanya, bukan HTML. Komponen halaman tersimpan pada resources/js/pages, dan router Inertia di browser bertugas menukar komponen tanpa memuat ulang halaman. Starter kit resmi Laravel untuk Vue dan React sudah menyertakan Inertia sejak awal, sehingga penyiapan manual terutama diperlukan pada proyek yang sudah berjalan.

Protokol permintaan: dua jenis kunjungan#

Inertia membedakan dua jenis kunjungan pada satu URL yang sama. Pada kunjungan pertama, server merender template akar menjadi HTML utuh dan menyematkan data halaman di dalamnya, sehingga konten awal tampil tanpa menunggu JavaScript selesai dimuat. Pada kunjungan berikutnya, komponen Link milik Inertia mengirim permintaan XHR, yaitu panggilan HTTP dari JavaScript tanpa navigasi halaman, dengan header X-Inertia. Server mengenali header tersebut dan menjawab JSON alih-alih HTML. Diagram berikut merangkum kedua jalur itu.

Browserkunjungan pertamaServer Laravelmerender template akarHTML utuhdata halaman tersematBrowserklik LinkServer Laraveldeteksi header X-InertiaJSON halamancomponent, props, url, version
Dua jenis kunjungan pada protokol Inertia
Muatan JSON-nya selalu berbentuk yang sama, yaitu kunci component berisi nama komponen halaman, props berisi datanya, url berisi alamat saat itu, dan version berisi penanda versi aset frontend. Adapter klien mencocokkan nama komponen dengan berkas pada resources/js/pages, menukar halaman yang tampil, lalu memperbarui riwayat browser. Dengan satu protokol ini, server tetap menjadi satu-satunya sumber rute dan data, sedangkan browser cukup mengetahui cara merender komponennya.

Penyiapan minimum pada proyek Laravel#

Instalasi dimulai dari adapter sisi server.

bash
# tidak-dijalankan
# memasang adapter server Inertia
cd ~/aplikasi-catatan
composer require inertiajs/inertia-laravel

Keluaran pemasangan pada salinan proyek berakhir dengan baris berikut.

text Hasil perintah composer require inertiajs/inertia-laravel
No security vulnerability advisories found.
Using version ^3.7 for inertiajs/inertia-laravel

Langkah kedua adalah menyiapkan template akar, yaitu satu-satunya template Blade yang dirender Inertia pada kunjungan pertama.

blade app.blade.php
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Aplikasi catatan</title>
    @vite(['resources/js/app.js'])
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>

Directive @vite memuat entry point JavaScript, @inertiaHead merender elemen head milik Inertia, dan @inertia menempatkan titik pemasangannya berupa <div id="app"></div>. Sejak versi 3, dokumentasi Inertia merekomendasikan komponen <x-inertia::head /> dan <x-inertia::app /> sebagai pengganti kedua directive tersebut, dan kedua bentuk itu tetap didukung.

Langkah ketiga adalah pendaftaran middleware. Perintah php artisan inertia:middleware membuat kelas App\Http\Middleware\HandleInertiaRequests, dan kelas tersebut didaftarkan ke grup web pada bootstrap/app.php seperti berikut.

php Pendaftaran middleware Inertia pada bootstrap/app.php
// tidak-dijalankan
->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        HandleInertiaRequests::class,
    ]);
})

Middleware ini membawa dua tanggung jawab. Metode share() mendefinisikan properti bersama yang dikirim ke setiap halaman, misalnya pesan error validasi, sedangkan metode version() menghitung penanda versi aset frontend yang digunakan untuk sinkronisasi setelah deploy.

Sisi klien memerlukan adapter beserta plugin Vite-nya.

bash
# tidak-dijalankan
# memasang adapter klien Vue beserta plugin Vite-nya
cd ~/aplikasi-catatan
npm install @inertiajs/vue3 @inertiajs/vite

Plugin tersebut ditambahkan pada vite.config.js, dan isi app.js cukup satu pemanggilan karena plugin mengurus resolusi halaman sekaligus pemasangannya.

js Konfigurasi vite.config.js dan berkas app.js
// tidak-dijalankan
// vite.config.js
import inertia from '@inertiajs/vite';

export default defineConfig({
    plugins: [
        laravel({ input: ['resources/js/app.js'], refresh: true }),
        inertia(),
    ],
});

// resources/js/app.js
import { createInertiaApp } from '@inertiajs/vue3';

createInertiaApp();

Resolver, yaitu fungsi yang mencocokkan nama komponen dengan berkasnya, adalah bagian bawaan plugin. Fungsi ini mencari komponen halaman pada direktori pages di sebelah app.js, yaitu resources/js/pages, kemudian memasang aplikasi ke elemen <div id="app"></div> dari template akar.

Rute pertama dan komponen halamannya#

Rute /catatan pada salinan proyek mengembalikan komponen Catatan/Index beserta dua properti.

php Rute /catatan pada routes/web.php
// tidak-dijalankan
Route::get('/catatan', function () {
    return Inertia::render('Catatan/Index', [
        'total' => Note::count(),
        'catatan' => Note::with('category')
            ->orderBy('judul')
            ->get()
            ->map(fn ($note) => [
                'id' => $note->id,
                'judul' => $note->judul,
                'kategori' => $note->category?->nama,
            ])
            ->all(),
    ]);
});

Nama Catatan/Index dipetakan ke berkas resources/js/pages/Catatan/Index.vue. Isinya masih sederhana karena seluruh pengambilan data sudah berlangsung di server. Relasi kategori dimuat dengan with('category') agar query tidak berulang per catatan, lalu map() mengubah setiap model menjadi array sederhana berisi id, judul, dan kategori, bentuk data yang aman dikirim sebagai JSON ke frontend.

vue Komponen halaman resources/js/pages/Catatan/Index.vue
// tidak-dijalankan
<script setup>
import { Link } from '@inertiajs/vue3'

defineProps({ total: Number, catatan: Array })
</script>

<template>
    <h1>Daftar catatan</h1>
    <p>{{ total }} catatan tersimpan.</p>

    <ul>
        <li v-for="note in catatan" :key="note.id">
            {{ note.judul }} <span v-if="note.kategori">&mdash; {{ note.kategori }}</span>
        </li>
    </ul>
</template>

Komponen Link digunakan untuk navigasi antarhalaman. Apabila tag <a> biasa digunakan, kunjungan kembali menjadi muat ulang halaman penuh karena Inertia hanya mengambil alih navigasi melalui komponen Link.

Kunjungan pertama dapat diperiksa tanpa browser. Dev server dijalankan terlebih dahulu, kemudian halaman diminta dengan curl dari terminal lain.

bash
# tidak-dijalankan
# menjalankan dev server lalu meminta /catatan tanpa browser
cd ~/aplikasi-catatan
php artisan serve
bash
# tidak-dijalankan
# meminta /catatan dari terminal lain
curl -s http://127.0.0.1:8000/catatan

Keluaran sungguhan dari percobaan pada salinan proyek adalah berikut.

text Hasil curl kunjungan pertama
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Aplikasi catatan</title>
    <link rel="modulepreload" as="script" href="http://127.0.0.1:8000/build/assets/app-BvRk9kiK.js" /><script type="module" src="http://127.0.0.1:8000/build/assets/app-BvRk9kiK.js" data-navigate-track="reload"></script>    </head>
<body>
    <script data-page="app" type="application/json">{"component":"Catatan\/Index","props":{"errors":{},"total":3,"catatan":[{"id":2,"judul":"Belajar Laravel Livewire","kategori":"Belajar"},{"id":3,"judul":"Daftar belanja mingguan","kategori":"Pribadi"},{"id":1,"judul":"Rapat koordinasi tim","kategori":"Kerja"}]},"url":"\/catatan","version":"f330e03c474c3847277efe2716adc44d","sharedProps":["errors"]}</script><div id="app"></div><script data-inertia-devtools-id type="application/json">"01M6H5H4F0AAMDT52SGC4NEZN9"</script></body>
</html>

Elemen script dengan atribut data-page memuat seluruh muatan halaman, yaitu component, props, url, dan version. Properti errors hadir tanpa diminta karena merupakan properti bersama dari middleware, sedangkan total dan catatan berasal dari rute. Nilai pengenal pada atribut data-inertia-devtools-id berbeda pada setiap permintaan karena digunakan oleh devtools, bukan bagian dari protokolnya.

Kunjungan berikutnya disimulasikan dengan mengirim header X-Inertia beserta versi aset yang cocok.

bash
# tidak-dijalankan
# menyimulasikan navigasi Inertia dari terminal
cd ~/aplikasi-catatan
curl -s -H "X-Inertia: true" -H "X-Inertia-Version: f330e03c474c3847277efe2716adc44d" http://127.0.0.1:8000/catatan

Keluarannya adalah JSON, bukan HTML.

text Hasil permintaan dengan header X-Inertia
{"component":"Catatan\/Index","props":{"errors":{},"total":3,"catatan":[{"id":2,"judul":"Belajar Laravel Livewire","kategori":"Belajar"},{"id":3,"judul":"Daftar belanja mingguan","kategori":"Pribadi"},{"id":1,"judul":"Rapat koordinasi tim","kategori":"Kerja"}]},"url":"\/catatan","version":"f330e03c474c3847277efe2716adc44d","sharedProps":["errors"]}

Router klien menerima JSON tersebut, menukar komponen halaman, dan memperbarui URL tanpa memuat ulang dokumen. Inilah sumber kesan SPA pada aplikasi Inertia: setiap navigasi menjadi permintaan kecil yang hanya membawa data.

Permintaan dapat dibatasi lebih lanjut apabila hanya sebagian properti yang berubah. Fitur partial reload diekspresikan melalui header X-Inertia-Partial-Data, dan server hanya mengevaluasi properti yang diminta.

bash
# tidak-dijalankan
# meminta ulang hanya properti catatan
cd ~/aplikasi-catatan
curl -s -H "X-Inertia: true" -H "X-Inertia-Version: f330e03c474c3847277efe2716adc44d" -H "X-Inertia-Partial-Component: Catatan/Index" -H "X-Inertia-Partial-Data: catatan" http://127.0.0.1:8000/catatan
text Hasil partial reload dengan only: catatan
{"component":"Catatan\/Index","props":{"errors":{},"catatan":[{"id":2,"judul":"Belajar Laravel Livewire","kategori":"Belajar"},{"id":3,"judul":"Daftar belanja mingguan","kategori":"Pribadi"},{"id":1,"judul":"Rapat koordinasi tim","kategori":"Kerja"}]},"url":"\/catatan","version":"f330e03c474c3847277efe2716adc44d","sharedProps":["errors"]}

Properti total tidak ada pada muatan tersebut karena tidak diminta, sedangkan properti bersama errors tetap disertakan server. Query yang mahal, misalnya agregasi atau tabel besar, cukup dievaluasi hanya ketika diminta. Kebalikannya tersedia melalui opsi except, yaitu daftar properti yang dikeluarkan dari muatan, dan karena partial reload hanya sah untuk komponen halaman yang sama, router.reload() yang menggunakan URL saat itu biasanya sudah cukup.

Kesalahan umum#

Kesalahan pertama adalah memberi nama komponen yang tidak sesuai berkasnya. Rute yang mengembalikan Catatan/Indeks sementara berkasnya bernama Catatan/Index.vue membuat resolver gagal menemukan komponen, sehingga halaman gagal dimuat dengan error pencarian komponen pada console browser. Nama direktori dan berkas di bawah resources/js/pages harus persis sama dengan nama yang dikirim Inertia::render().

Kesalahan kedua adalah salah mengartikan respons 409 saat pengembangan. Apabila versi aset berubah setelah build, kunjungan XHR yang membawa versi lama dijawab sebagai berikut.

bash
# tidak-dijalankan
# mengirim versi aset yang sudah tidak berlaku
cd ~/aplikasi-catatan
curl -s -i -H "X-Inertia: true" -H "X-Inertia-Version: 0000" http://127.0.0.1:8000/catatan

Baris teratas keluaran percobaan tersebut adalah berikut.

text Hasil permintaan dengan versi aset yang tidak cocok
HTTP/1.1 409 Conflict
Host: 127.0.0.1:8000
Connection: close
X-Powered-By: PHP/8.3.29
X-Inertia-Location: http://127.0.0.1:8000/catatan

Header X-Inertia-Location berisi alamat yang dimuat ulang secara penuh oleh adapter klien, sehingga browser kembali sinkron dengan aset terbaru. Perilaku ini adalah mekanisme sinkronisasi yang disengaja, bukan error aplikasi.

Kesalahan ketiga adalah melewatkan pendaftaran middleware. Pada salinan proyek yang middlewarenya belum didaftarkan, properti errors tidak muncul pada muatan dan version bernilai kosong, sehingga properti bersama dan sinkronisasi aset tidak berjalan. Perbaikannya adalah menjalankan php artisan inertia:middleware lalu mendaftarkan kelasnya pada grup web seperti pada bagian penyiapan.

Ringkasan#

  • Inertia menghubungkan Laravel dengan Vue atau React melalui dua adapter, sehingga routing, validasi, dan otorisasi tetap berada di PHP tanpa API JSON terpisah.
  • Kunjungan pertama menghasilkan HTML utuh dengan muatan data-page, sedangkan kunjungan berikutnya adalah permintaan XHR ber-header X-Inertia yang dijawab JSON berisi component, props, url, dan version.
  • Penyiapan minimumnya mencakup adapter server, template akar app.blade.php, middleware HandleInertiaRequests, serta adapter klien dengan plugin Vite.
  • Partial reload membatasi properti yang dievaluasi ulang, dan ketidakcocokan version dijawab 409 dengan header X-Inertia-Location agar klien memuat ulang halaman.

Referensi#