laravel-inertia
Auto-invoked skill
Build SPAs with Laravel and Inertia.js using Vue or React
Trigger Keywords
This skill automatically activates when Claude detects these keywords:
Overview
The laravel-inertia skill provides expertise for building modern single-page applications with Laravel and Inertia.js. Inertia allows you to build SPAs using classic server-side routing and controllers, without building an API.
What This Skill Provides
- Framework Support - Vue 3, React, or Svelte integration
- Server-Side Routing - No client-side router needed
- Form Handling - useForm helper with validation
- Partial Reloads - Optimize data fetching
- SSR Support - Server-side rendering for SEO
- File Uploads - Native FormData support with progress
Example Conversations
# Building a SPA
"Create a product management SPA with Inertia and Vue"
# Forms
"How do I handle form validation with Inertia?"
# Optimization
"Implement partial reloads for better performance"
# SEO
"Set up server-side rendering for my Inertia app"
Quick Start with Breeze
The fastest way to start with Inertia is using Laravel Breeze:
# Install Breeze with Vue
composer require laravel/breeze --dev
php artisan breeze:install vue
# Or with React
php artisan breeze:install react
# Or with TypeScript
php artisan breeze:install vue --typescript
# Build assets
npm install
npm run dev
Controller Pattern
Return Inertia responses from your controllers:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Inertia\Inertia;
use Inertia\Response;
class ProductController extends Controller
{
public function index(): Response
{
return Inertia::render('Products/Index', [
'products' => Product::query()
->with('category')
->latest()
->paginate(15)
->through(fn ($product) => [
'id' => $product->id,
'name' => $product->name,
'price' => $product->price_formatted,
'category' => $product->category->name,
]),
'filters' => request()->only(['search', 'category']),
]);
}
public function store(StoreProductRequest $request): RedirectResponse
{
Product::create($request->validated());
return redirect()
->route('products.index')
->with('success', 'Product created successfully.');
}
}
Vue Page Component
<!-- resources/js/Pages/Products/Index.vue -->
<script setup>
import { Head, Link, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
defineProps({
products: Object,
filters: Object,
})
const deleteProduct = (id) => {
if (confirm('Are you sure?')) {
router.delete(route('products.destroy', id))
}
}
</script>
<template>
<AppLayout>
<Head title="Products" />
<div class="flex justify-between mb-6">
<h1 class="text-2xl font-bold">Products</h1>
<Link :href="route('products.create')" class="btn">
Create Product
</Link>
</div>
<table>
<tbody>
<tr v-for="product in products.data" :key="product.id">
<td></td>
<td></td>
<td>
<Link :href="route('products.edit', product.id)">
Edit
</Link>
<button @click="deleteProduct(product.id)">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<!-- Pagination -->
<div class="flex justify-between">
<Link v-if="products.prev_page_url" :href="products.prev_page_url">
Previous
</Link>
<Link v-if="products.next_page_url" :href="products.next_page_url">
Next
</Link>
</div>
</AppLayout>
</template>
Forms with useForm
The useForm helper provides form state management and validation:
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
name: '',
price: '',
category_id: null,
})
const submit = () => {
form.post(route('products.store'), {
preserveScroll: true,
onSuccess: () => form.reset(),
})
}
</script>
<template>
<form @submit.prevent="submit">
<input
v-model="form.name"
type="text"
:class="{ 'border-red-500': form.errors.name }"
/>
<div v-if="form.errors.name" class="text-red-500">
</div>
<input v-model="form.price" type="number" step="0.01" />
<button type="submit" :disabled="form.processing">
<span v-if="form.processing">Creating...</span>
<span v-else>Create Product</span>
</button>
</form>
</template>
File Uploads
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
name: '',
image: null,
})
const handleFileChange = (e) => {
form.image = e.target.files[0]
}
const submit = () => {
form.post(route('products.store'), {
forceFormData: true, // Required for file uploads
preserveScroll: true,
})
}
</script>
<template>
<form @submit.prevent="submit">
<input type="file" @change="handleFileChange" accept="image/*" />
<!-- Preview -->
<img
v-if="form.image"
:src="URL.createObjectURL(form.image)"
class="h-32 w-32"
/>
<!-- Upload Progress -->
<div v-if="form.progress">
<div class="bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full"
:style="{ width: `${form.progress.percentage}%` }"
></div>
</div>
<p>% uploaded</p>
</div>
<button type="submit" :disabled="form.processing">Upload</button>
</form>
</template>
Shared Data (Middleware)
Share data across all pages using HandleInertiaRequests middleware:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'permissions' => $request->user()->permissions,
] : null,
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
'errors' => fn () => $request->session()->get('errors')
?->getBag('default')?->getMessages() ?? (object) [],
];
}
}
Partial Reloads
Only reload specific props for better performance:
<script setup>
import { router } from '@inertiajs/vue3'
const filterByCategory = (categoryId) => {
router.get(
route('products.index'),
{ category: categoryId },
{
only: ['products', 'filters'], // Only reload these props
preserveState: true,
}
)
}
const loadMore = () => {
router.reload({
only: ['products'],
preserveScroll: true,
preserveState: true,
})
}
</script>
Lazy Props
Defer expensive data loading until explicitly requested:
// Controller
return Inertia::render('Products/Show', [
'product' => $product,
// Only load reviews when explicitly requested
'reviews' => Inertia::lazy(fn () => $product->reviews()->latest()->get()),
]);
<script setup>
import { router } from '@inertiajs/vue3'
const loadReviews = () => {
router.reload({ only: ['reviews'] })
}
</script>
SEO with Head Component
<script setup>
import { Head } from '@inertiajs/vue3'
defineProps({
product: Object,
})
</script>
<template>
<div>
<Head>
<title></title>
<meta name="description" :content="product.description" />
<meta property="og:title" :content="product.name" />
<meta property="og:image" :content="product.image_url" />
</Head>
<!-- Page content -->
</div>
</template>
Testing Inertia Apps
<?php
use Inertia\Testing\AssertableInertia as Assert;
it('renders products index page', function () {
Product::factory()->count(3)->create();
$this->get(route('products.index'))
->assertInertia(fn (Assert $page) => $page
->component('Products/Index')
->has('products.data', 3)
->has('products.data.0', fn (Assert $page) => $page
->where('id', Product::first()->id)
->has('name')
->has('price')
->etc()
)
);
});
it('shares auth user data', function () {
$this->actingAs($user = User::factory()->create());
$this->get(route('products.index'))
->assertInertia(fn (Assert $page) => $page
->has('auth.user', fn (Assert $page) => $page
->where('id', $user->id)
->where('name', $user->name)
->etc()
)
);
});
Common Pitfalls
1. Not Using router for Navigation
Problem: Using regular <a> tags breaks SPA experience
<!-- Bad -->
<a href="/products">Products</a>
<!-- Good -->
<Link :href="route('products.index')">Products</Link>
2. Forgetting forceFormData for File Uploads
Problem: Files aren't uploaded without FormData
// Bad
form.post(route('products.store'))
// Good
form.post(route('products.store'), {
forceFormData: true,
})
3. Not Preserving Scroll on Updates
Problem: User loses position after actions
// Bad
router.delete(route('products.destroy', id))
// Good
router.delete(route('products.destroy', id), {
preserveScroll: true,
})
4. Returning Full Models to Frontend
Problem: Exposes sensitive data and increases payload
// Bad
return Inertia::render('Products/Index', [
'products' => Product::all(),
]);
// Good
return Inertia::render('Products/Index', [
'products' => Product::all()->map(fn ($p) => [
'id' => $p->id,
'name' => $p->name,
'price' => $p->price_formatted,
]),
]);
5. N+1 Queries in Props
Problem: Causes performance issues
// Bad
return Inertia::render('Products/Index', [
'products' => Product::all(), // N+1 when accessing relations
]);
// Good
return Inertia::render('Products/Index', [
'products' => Product::with('category')->get(),
]);
6. Not Using Lazy Props for Heavy Data
Problem: Slows initial page load
// Bad
return Inertia::render('Dashboard', [
'stats' => $this->calculateHeavyStats(),
]);
// Good
return Inertia::render('Dashboard', [
'stats' => Inertia::lazy(fn () => $this->calculateHeavyStats()),
]);
Best Practices
- Transform Data in Controllers - Don't expose raw models to the frontend
- Use Partial Reloads - Only reload necessary data for better performance
- Implement Loading States - Show feedback during form submissions
- Preserve Scroll Intelligently - Better UX for list updates
- Use Lazy Props for Heavy Data - Faster initial page loads
- Share Only Necessary User Data - Security and performance
- Use Ziggy for Route Helpers - Type-safe routing in JavaScript
- Implement Proper Error Handling - Display validation errors properly
- Use SSR for SEO-Critical Pages - Better search rankings
- Test Inertia Assertions - Verify props and components are correct
Package Integration
- tightenco/ziggy - Laravel routes in JavaScript
- spatie/laravel-query-builder - Advanced filtering and sorting
- spatie/laravel-medialibrary - File uploads and media management
- laravel/sanctum - API authentication if needed
Related Commands
/laravel-agent:inertia:make- Create Inertia page components/laravel-agent:inertia:install- Setup Inertia with Vue or React/laravel-agent:breeze:install- Install Laravel Breeze with Inertia
Related Skills
- laravel-api - Building REST APIs (alternative approach)
- laravel-livewire - Alternative reactive framework
- laravel-feature - Feature structure patterns
- laravel-testing - Testing strategies