Laravel + Inertia.js + React integration patterns. Use when building Inertia page components, handling forms with useForm, managing shared data, or implementing persistent layouts. Triggers on tasks involving Inertia.js, page props, form handling, or Laravel React integration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/inertia-react:laravel-inertia-reactThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive patterns for building modern monolithic applications with Laravel, Inertia.js, and React. Contains 30+ rules for seamless full-stack development.
AGENTS.mdREADME.mdrules/_sections.mdrules/_template.mdrules/form-dirty-tracking.mdrules/form-file-uploads.mdrules/form-progress-indicator.mdrules/form-reset-handling.mdrules/form-transform-data.mdrules/form-useform-basic.mdrules/form-useform-hook.mdrules/form-validation-errors.mdrules/layout-persistent.mdrules/nav-external-links.mdrules/nav-link-component.mdrules/nav-preserve-state.mdrules/nav-programmatic.mdrules/nav-replace-history.mdrules/page-component-structure.mdrules/page-default-layout.mdComprehensive patterns for building modern monolithic applications with Laravel, Inertia.js, and React. Contains 30+ rules for seamless full-stack development.
Reference these guidelines when:
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Page Components | CRITICAL | page- |
| 2 | Forms & Validation | CRITICAL | form- |
| 3 | Navigation & Links | HIGH | nav- |
| 4 | Shared Data | HIGH | shared- |
| 5 | Layouts | MEDIUM | layout- |
| 6 | File Uploads | MEDIUM | upload- |
| 7 | Advanced Patterns | LOW | advanced- |
page-props-typing - Type page props from Laravelpage-component-structure - Standard page component patternpage-head-management - Title and meta tags with Headpage-default-layout - Assign layouts to pagesform-useform-basic - Basic useForm usageform-validation-errors - Display Laravel validation errorsform-processing-state - Handle form submission stateform-reset-preserve - Reset vs preserve form dataform-nested-data - Handle nested form dataform-transform - Transform data before submitnav-link-component - Use Link for navigationnav-preserve-state - Preserve scroll and statenav-partial-reloads - Reload only what changednav-replace-history - Replace vs push historyshared-auth-user - Access authenticated usershared-flash-messages - Handle flash messagesshared-global-props - Access global propsshared-typescript - Type shared datalayout-persistent - Persistent layouts patternlayout-nested - Nested layoutslayout-default - Default layout assignmentlayout-conditional - Conditional layoutsupload-basic - Basic file uploadupload-progress - Upload progress trackingupload-multiple - Multiple file uploadsadvanced-polling - Real-time pollingadvanced-prefetch - Prefetch pagesadvanced-modal-pages - Modal as pagesadvanced-infinite-scroll - Infinite scrolling// resources/js/Pages/Posts/Index.tsx
import { Head, Link } from '@inertiajs/react'
interface Post {
id: number
title: string
excerpt: string
created_at: string
author: {
id: number
name: string
}
}
interface Props {
posts: {
data: Post[]
links: { url: string | null; label: string; active: boolean }[]
}
filters: {
search?: string
}
}
export default function Index({ posts, filters }: Props) {
return (
<>
<Head title="Posts" />
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Posts</h1>
<div className="space-y-4">
{posts.data.map((post) => (
<article key={post.id} className="p-4 bg-white rounded-lg shadow">
<Link href={route('posts.show', post.id)}>
<h2 className="text-xl font-semibold hover:text-blue-600">
{post.title}
</h2>
</Link>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
<p className="text-sm text-gray-400 mt-2">
By {post.author.name}
</p>
</article>
))}
</div>
</div>
</>
)
}
// resources/js/Pages/Posts/Create.tsx
import { Head, useForm, Link } from '@inertiajs/react'
import { FormEvent } from 'react'
interface Category {
id: number
name: string
}
interface Props {
categories: Category[]
}
export default function Create({ categories }: Props) {
const { data, setData, post, processing, errors, reset } = useForm({
title: '',
body: '',
category_id: '',
})
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
post(route('posts.store'), {
onSuccess: () => reset(),
})
}
return (
<>
<Head title="Create Post" />
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto py-8">
<div className="mb-4">
<label htmlFor="title" className="block font-medium mb-1">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="w-full border rounded px-3 py-2"
/>
{errors.title && (
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="category" className="block font-medium mb-1">
Category
</label>
<select
id="category"
value={data.category_id}
onChange={(e) => setData('category_id', e.target.value)}
className="w-full border rounded px-3 py-2"
>
<option value="">Select a category</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
{errors.category_id && (
<p className="text-red-500 text-sm mt-1">{errors.category_id}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="body" className="block font-medium mb-1">
Content
</label>
<textarea
id="body"
value={data.body}
onChange={(e) => setData('body', e.target.value)}
rows={10}
className="w-full border rounded px-3 py-2"
/>
{errors.body && (
<p className="text-red-500 text-sm mt-1">{errors.body}</p>
)}
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={processing}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{processing ? 'Creating...' : 'Create Post'}
</button>
<Link
href={route('posts.index')}
className="px-4 py-2 border rounded"
>
Cancel
</Link>
</div>
</form>
</>
)
}
// resources/js/Layouts/AppLayout.tsx
import { Link, usePage } from '@inertiajs/react'
import { ReactNode } from 'react'
interface Props {
children: ReactNode
}
export default function AppLayout({ children }: Props) {
const { auth } = usePage().props as { auth: { user: { name: string } } }
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow">
<div className="container mx-auto px-4 py-3 flex justify-between">
<Link href="/" className="font-bold">
My App
</Link>
<span>Welcome, {auth.user.name}</span>
</div>
</nav>
<main className="container mx-auto px-4 py-8">
{children}
</main>
</div>
)
}
// resources/js/Pages/Dashboard.tsx
import AppLayout from '@/Layouts/AppLayout'
export default function Dashboard() {
return <h1>Dashboard</h1>
}
// Assign persistent layout
Dashboard.layout = (page: ReactNode) => <AppLayout>{page}</AppLayout>
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class PostController extends Controller
{
public function index(): Response
{
return Inertia::render('Posts/Index', [
'posts' => Post::with('author:id,name')
->latest()
->paginate(10),
'filters' => request()->only('search'),
]);
}
public function create(): Response
{
return Inertia::render('Posts/Create', [
'categories' => Category::all(['id', 'name']),
]);
}
public function store(StorePostRequest $request): RedirectResponse
{
$post = Post::create([
...$request->validated(),
'user_id' => auth()->id(),
]);
return redirect()
->route('posts.show', $post)
->with('success', 'Post created successfully.');
}
public function show(Post $post): Response
{
return Inertia::render('Posts/Show', [
'post' => $post->load('author', 'category'),
]);
}
}
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
] : null,
],
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
]);
}
}
// resources/js/Components/FlashMessages.tsx
import { usePage } from '@inertiajs/react'
import { useEffect, useState } from 'react'
export default function FlashMessages() {
const { flash } = usePage().props as {
flash: { success?: string; error?: string }
}
const [visible, setVisible] = useState(false)
useEffect(() => {
if (flash.success || flash.error) {
setVisible(true)
const timer = setTimeout(() => setVisible(false), 3000)
return () => clearTimeout(timer)
}
}, [flash])
if (!visible) return null
return (
<div className="fixed top-4 right-4 z-50">
{flash.success && (
<div className="bg-green-500 text-white px-4 py-2 rounded shadow">
{flash.success}
</div>
)}
{flash.error && (
<div className="bg-red-500 text-white px-4 py-2 rounded shadow">
{flash.error}
</div>
)}
</div>
)
}
Read individual rule files for detailed explanations and code examples:
rules/form-useform-basic.md
rules/page-props-typing.md
rules/layout-persistent.md
laravel-inertia-react/
├── SKILL.md # This file - overview and examples
├── README.md # Quick reference guide
├── AGENTS.md # Integration guide for AI agents
├── metadata.json # Skill metadata and references
└── rules/
├── _sections.md # Rule categories and priorities
├── _template.md # Template for new rules
├── page-*.md # Page component patterns (6 rules)
├── form-*.md # Form handling patterns (8 rules)
├── nav-*.md # Navigation patterns (5 rules)
├── shared-*.md # Shared data patterns (4 rules)
└── layout-*.md # Layout patterns (1 rule)
MIT License. This skill is provided as-is for educational and development purposes.
npx claudepluginhub noppu-labs/ai-toolkit --plugin inertia-reactGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.