Masalah Hard Reload pada Arsitektur Inertia.js

Inertia.js bekerja dengan memotong navigasi browser tradisional dan menggantinya dengan permintaan XHR (X-Inertia). Saat pengembang secara tidak sengaja menggunakan tag native HTML <a href="..."> atau <form action="...">, browser akan melakukan hard reload secara penuh (full page reload). Hal ini merusak state aplikasi di memory, memicu unmount pada persistent layout, mengunduh ulang aset CSS/JS, dan merusak performa Single Page Application (SPA).

Solusi deterministik untuk mencegah regresi ini adalah dengan menambahkan aturan linting statis berbasis Abstract Syntax Tree (AST) melalui ESLint, yang dijalankan otomatis pada siklus git pre-commit dan CI pipeline.

Konfigurasi ESLint: no-restricted-syntax

ESLint menyediakan rule bawaan no-restricted-syntax yang memanfaatkan selector AST (esquery). Aturan ini dapat memblokir tag native JSX <a> dan <form>, sambil tetap mengecualikan tautan keluar atau file download.

Implementasi pada eslint.config.js (Flat Config)

import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    files: ["resources/js/**/*.{jsx,tsx}"],
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector: "JSXOpeningElement[name.name='a']:not(:has(JSXAttribute[name.name='target'][value.value='_blank'])):not(:has(JSXAttribute[name.name='download'])):not(:has(JSXAttribute[name.name='rel'][value.value='external']))",
          message: "Gunakan komponen <Link> dari @inertiajs alih-alih <a> native untuk navigasi internal SPA."
        },
        {
          selector: "JSXOpeningElement[name.name='form'][attributes.length>0]:has(JSXAttribute[name.name='action'])",
          message: "Hindari <form action='...'>. Gunakan hook useForm() atau router.visit() untuk mutasi data Inertia."
        }
      ]
    }
  }
];

Penjelasan AST Selector

  • JSXOpeningElement[name.name='a']: Menargetkan elemen JSX pembuka bernama a.
  • :not(:has(...)): Selector negasi. Jika elemen memiliki atribut target="_blank", download, atau rel="external", rule tidak akan menandai elemen tersebut sebagai error.
  • JSXAttribute[name.name='action']: Memblokir form native yang mengandalkan submit default HTML yang memicu perpindahan halaman non-AJAX.
Catatan untuk Pengguna Vue: Jika menggunakan Vue SFC, gunakan rule vue/no-restricted-syntax dari package eslint-plugin-vue dengan selector setara berbasis template AST: VElement[name='a']:not([target='_blank']).

Menangani Kasus Edge Case: Eksternal URL & File Download

Validasi AST statis rentan menimbulkan false positive bila tidak memperhitungkan rute non-SPA. Tiga skenario di mana tag native tetap valid:

  1. External Links: Menuju domain pihak ketiga. Selalu wajibkan atribut target="_blank" atau rel="external" agar linter mengizinkan penggunaan tag <a>.
  2. File Download / Direct Assets: Unduhan PDF atau export spreadsheet dari controller Laravel (bukan respons Inertia). Gunakan atribut HTML5 download pada tag <a>.
  3. Anchor / In-page Hash: Navigasi internal ID seperti href="#section-faq". Jika diperlukan, tambahkan pengecualian AST untuk atribut href yang diawali karakter #.
// Contoh penggunaan yang lolos linting
<!-- Navigasi Internal (Benar) -->
<Link href="/dashboard">Dashboard</Link>

<!-- Eksternal URL (Lolos selector: memiliki target="_blank") -->
<a href="https://docs.example.com" target="_blank" rel="noopener noreferrer">Docs</a>

<!-- File Download (Lolos selector: memiliki atribut download) -->
<a href="/reports/export-csv" download>Unduh Laporan</a>

Otomasi Pre-Commit: Husky & lint-staged

Linting harus dijalankan pada fase sedini mungkin. Menggunakan Husky bersama lint-staged memastikan pengembang tidak dapat melakukan commit kode yang melanggar standar.

1. Instalasi Dependency

npm install --save-dev husky lint-staged
npx husky init

2. Konfigurasi package.json

{
  "lint-staged": {
    "resources/js/**/*.{js,jsx,ts,tsx,vue}": [
      "eslint --fix --max-warnings=0"
    ]
  }
}

3. Konfigurasi Hook .husky/pre-commit

npx lint-staged

Integrasi pada GitHub Actions CI Pipeline

Pre-commit lokal dapat dilewati pengembang menggunakan flag --no-verify. Continuous Integration (CI) berfungsi sebagai enforcement mutlak sebelum kode di-merge ke branch utama.

name: Frontend Code Quality

on:
  pull_request:
    branches: [main, master]
  push:
    branches: [main, master]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Run ESLint
        run: npx eslint resources/js/ --max-warnings=0

Flag --max-warnings=0 memastikan eksekusi CI langsung gagal (exit code non-zero) jika ditemukan satupun peringatan, memaksa tim mematuhi navigasi berbasis SPA secara konsisten.

Trade-offs dan Strategi Debugging

  • Dynamic Hrefs: ESLint statis tidak dapat mengevaluasi apakah variabel URL <a href={dynamicUrl}> mengarah ke rute internal atau eksternal pada runtime. Tetapkan konvensi tim: jika link dinamis berpotensi eksternal, bungkus dalam utilitas custom wrapper link.
  • Escape Hatch Terkontrol: Jika terdapat kebutuhan khusus menggunakan tag <a> internal (misalnya me-reset total memory leak aplikasi), gunakan comment ignore spesifik: // eslint-disable-next-line no-restricted-syntax dan sertakan alasan di atas komentar tersebut.