Back to skills
SkillHub ClubAnalyze Data & AIFull StackFrontendData / AI

laravel-inertia-react

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.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
13
Hot score
85
Updated
March 20, 2026
Overall rating
C2.5
Composite score
2.5
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install asyrafhussin-agent-skills-laravel-inertia-react

Repository

asyrafhussin/agent-skills

Skill path: skills/laravel-inertia-react

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.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Frontend, Data / AI, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: asyrafhussin.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install laravel-inertia-react into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/asyrafhussin/agent-skills before adding laravel-inertia-react to shared team environments
  • Use laravel-inertia-react for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: laravel-inertia-react
description: 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.
---

# Laravel + Inertia.js + React

Comprehensive patterns for building modern monolithic applications with Laravel, Inertia.js, and React. Contains 30+ rules for seamless full-stack development.

## When to Apply

Reference these guidelines when:
- Creating Inertia page components
- Handling forms with useForm hook
- Managing shared data and authentication
- Implementing persistent layouts
- Navigating between pages

## Rule Categories by Priority

| 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-` |

## Quick Reference

### 1. Page Components (CRITICAL)

- `page-props-typing` - Type page props from Laravel
- `page-component-structure` - Standard page component pattern
- `page-head-management` - Title and meta tags with Head
- `page-default-layout` - Assign layouts to pages

### 2. Forms & Validation (CRITICAL)

- `form-useform-basic` - Basic useForm usage
- `form-validation-errors` - Display Laravel validation errors
- `form-processing-state` - Handle form submission state
- `form-reset-preserve` - Reset vs preserve form data
- `form-nested-data` - Handle nested form data
- `form-transform` - Transform data before submit

### 3. Navigation & Links (HIGH)

- `nav-link-component` - Use Link for navigation
- `nav-preserve-state` - Preserve scroll and state
- `nav-partial-reloads` - Reload only what changed
- `nav-replace-history` - Replace vs push history

### 4. Shared Data (HIGH)

- `shared-auth-user` - Access authenticated user
- `shared-flash-messages` - Handle flash messages
- `shared-global-props` - Access global props
- `shared-typescript` - Type shared data

### 5. Layouts (MEDIUM)

- `layout-persistent` - Persistent layouts pattern
- `layout-nested` - Nested layouts
- `layout-default` - Default layout assignment
- `layout-conditional` - Conditional layouts

### 6. File Uploads (MEDIUM)

- `upload-basic` - Basic file upload
- `upload-progress` - Upload progress tracking
- `upload-multiple` - Multiple file uploads

### 7. Advanced Patterns (LOW)

- `advanced-polling` - Real-time polling
- `advanced-prefetch` - Prefetch pages
- `advanced-modal-pages` - Modal as pages
- `advanced-infinite-scroll` - Infinite scrolling

## Essential Patterns

### Page Component with TypeScript

```tsx
// 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>
    </>
  )
}
```

### Form with useForm

```tsx
// 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>
    </>
  )
}
```

### Persistent Layout

```tsx
// 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>
```

### Laravel Controller

```php
<?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'),
        ]);
    }
}
```

### Shared Data (HandleInertiaRequests)

```php
<?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'),
            ],
        ]);
    }
}
```

### Flash Messages Component

```tsx
// 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>
  )
}
```

## How to Use

Read individual rule files for detailed explanations and code examples:

```
rules/form-useform-basic.md
rules/page-props-typing.md
rules/layout-persistent.md
```

## Project Structure

```
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)
```

## References

- [Inertia.js Documentation](https://inertiajs.com/) - Official Inertia.js docs
- [Laravel Documentation](https://laravel.com/docs) - Laravel framework docs
- [React Documentation](https://react.dev/) - Official React docs
- [Ziggy](https://github.com/tighten/ziggy) - Laravel route helper for JavaScript

## License

MIT License

Copyright (c) 2026 Asyraf Hussin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

## Metadata

- **Version**: 1.0.0
- **Last Updated**: 2026-01-17
- **Maintainer**: Asyraf Hussin
- **Rule Count**: 24 rules across 6 categories
- **Tech Stack**: Laravel 10+, Inertia.js 1.0+, React 18+, TypeScript 5+


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### rules/form-useform-basic.md

```markdown
---
section: forms
priority: critical
description: Use useForm hook for form state management and Laravel validation integration
keywords: [useForm, form, validation, laravel, state]
---

# Form useForm Hook

The useForm hook is Inertia's primary way to handle forms. It provides automatic form state management, error handling, processing state, and seamless integration with Laravel validation.

## Incorrect

```tsx
// ❌ Manual state management
import { useState } from 'react'
import { router } from '@inertiajs/react'

function CreatePost() {
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')
  const [errors, setErrors] = useState({})
  const [processing, setProcessing] = useState(false)

  const handleSubmit = async (e) => {
    e.preventDefault()
    setProcessing(true)

    router.post('/posts', { title, body }, {
      onError: (errors) => setErrors(errors),
      onFinish: () => setProcessing(false),
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* ... */}
    </form>
  )
}
```

## Correct

```tsx
// ✅ Using useForm hook
import { useForm } from '@inertiajs/react'
import { FormEvent } from 'react'

interface FormData {
  title: string
  body: string
  category_id: string
}

export default function CreatePost() {
  const { data, setData, post, processing, errors, reset, clearErrors } =
    useForm<FormData>({
      title: '',
      body: '',
      category_id: '',
    })

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault()

    post(route('posts.store'), {
      onSuccess: () => {
        reset()  // Clear form on success
      },
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          type="text"
          value={data.title}
          onChange={(e) => setData('title', e.target.value)}
          onFocus={() => clearErrors('title')}
        />
        {errors.title && <span className="text-red-500">{errors.title}</span>}
      </div>

      <div>
        <label htmlFor="body">Body</label>
        <textarea
          id="body"
          value={data.body}
          onChange={(e) => setData('body', e.target.value)}
        />
        {errors.body && <span className="text-red-500">{errors.body}</span>}
      </div>

      <button type="submit" disabled={processing}>
        {processing ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  )
}
```

## useForm Methods

```tsx
const {
  data,           // Current form data
  setData,        // Update form data
  post,           // POST request
  put,            // PUT request
  patch,          // PATCH request
  delete: destroy,// DELETE request (renamed to avoid keyword)
  processing,     // Is form submitting?
  errors,         // Validation errors from Laravel
  reset,          // Reset form to initial values
  clearErrors,    // Clear specific or all errors
  isDirty,        // Has form been modified?
  transform,      // Transform data before submit
  setError,       // Set custom error
  recentlySuccessful, // Was last submit successful?
} = useForm({
  // Initial values
})
```

## Setting Data

```tsx
// Single field
setData('title', 'New Title')

// Multiple fields
setData({
  title: 'New Title',
  body: 'New Body',
})

// Using callback (access previous data)
setData((prevData) => ({
  ...prevData,
  title: prevData.title.toUpperCase(),
}))

// Nested data
setData('author.name', 'John')
```

## HTTP Methods

```tsx
// POST - Create
post(route('posts.store'))

// PUT - Full update
put(route('posts.update', post.id))

// PATCH - Partial update
patch(route('posts.update', post.id))

// DELETE
destroy(route('posts.destroy', post.id), {
  onBefore: () => confirm('Are you sure?'),
})
```

## Options

```tsx
post(route('posts.store'), {
  // Preserve state on success
  preserveState: true,

  // Preserve scroll position
  preserveScroll: true,

  // Replace history instead of push
  replace: true,

  // Callbacks
  onBefore: (visit) => {
    // Return false to cancel
  },
  onStart: (visit) => {},
  onProgress: (progress) => {
    // For file uploads
    console.log(progress.percentage)
  },
  onSuccess: (page) => {
    reset()
  },
  onError: (errors) => {
    // Handle errors
  },
  onCancel: () => {},
  onFinish: () => {
    // Always called (success or error)
  },
})
```

## Edit Form Pattern

```tsx
interface Post {
  id: number
  title: string
  body: string
}

interface Props {
  post: Post
}

export default function Edit({ post }: Props) {
  // Initialize with existing data
  const { data, setData, put, processing, errors } = useForm({
    title: post.title,
    body: post.body,
  })

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault()
    put(route('posts.update', post.id))
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button disabled={processing}>
        {processing ? 'Saving...' : 'Save Changes'}
      </button>
    </form>
  )
}
```

## Transform Data Before Submit

```tsx
const { data, setData, transform, post } = useForm({
  remember: false,
  email: '',
  password: '',
})

// Transform before sending
transform((data) => ({
  ...data,
  remember: data.remember ? 'on' : '',
}))
```

## Benefits

- Automatic error handling from Laravel
- Processing state management
- Form reset functionality
- TypeScript support
- Seamless Laravel integration

```

### rules/layout-persistent.md

```markdown
---
section: layouts
priority: critical
description: Implement persistent layouts that maintain state across navigation
keywords: [layout, persistent, state, performance, remount, nested-layouts]
---

# Persistent Layouts

Persistent layouts maintain state between page visits. Without them, layouts remount on every navigation, losing state like scroll position, form inputs in navigation, or audio/video playback.

## Incorrect

```tsx
// ❌ Layout wrapping in page - remounts on every navigation
import AppLayout from '@/Layouts/AppLayout'

export default function Dashboard() {
  return (
    <AppLayout>
      <h1>Dashboard</h1>
    </AppLayout>
  )
}

// ❌ Layout in _app.jsx - still remounts
function App({ Component, pageProps }) {
  return (
    <AppLayout>
      <Component {...pageProps} />
    </AppLayout>
  )
}
```

**Problem:** Layout remounts on every page change, losing any state.

## Correct

### Using layout Property

```tsx
// 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 any

  return (
    <div className="min-h-screen bg-gray-100">
      <nav className="bg-white shadow-sm">
        <div className="max-w-7xl mx-auto px-4 py-3">
          <div className="flex justify-between items-center">
            <div className="flex space-x-4">
              <Link href="/" className="font-bold">
                Logo
              </Link>
              <Link href="/dashboard">Dashboard</Link>
              <Link href="/posts">Posts</Link>
            </div>
            <span>{auth.user?.name}</span>
          </div>
        </div>
      </nav>

      <main className="max-w-7xl mx-auto py-6 px-4">
        {children}
      </main>
    </div>
  )
}
```

```tsx
// resources/js/Pages/Dashboard.tsx
import AppLayout from '@/Layouts/AppLayout'
import { ReactNode } from 'react'

export default function Dashboard() {
  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>
      {/* Page content */}
    </div>
  )
}

// Assign persistent layout
Dashboard.layout = (page: ReactNode) => <AppLayout>{page}</AppLayout>
```

### TypeScript Declaration

```tsx
// resources/js/types/inertia.d.ts
import { ReactNode } from 'react'

declare module '@inertiajs/react' {
  interface PageComponent {
    layout?: (page: ReactNode) => ReactNode
  }
}
```

### Default Layout in app.tsx

```tsx
// resources/js/app.tsx
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/**/*.tsx', { eager: true })
    const page = pages[`./Pages/${name}.tsx`] as any

    // Set default layout if page doesn't have one
    page.default.layout = page.default.layout || ((page: ReactNode) =>
      <AppLayout>{page}</AppLayout>
    )

    return page
  },
  setup({ el, App, props }) {
    createRoot(el!).render(<App {...props} />)
  },
})
```

### Nested Layouts

```tsx
// resources/js/Layouts/SettingsLayout.tsx
import { Link } from '@inertiajs/react'
import { ReactNode } from 'react'

interface Props {
  children: ReactNode
}

export default function SettingsLayout({ children }: Props) {
  return (
    <div className="flex">
      <aside className="w-64 border-r p-4">
        <nav className="space-y-2">
          <Link href="/settings/profile">Profile</Link>
          <Link href="/settings/password">Password</Link>
          <Link href="/settings/notifications">Notifications</Link>
        </nav>
      </aside>
      <main className="flex-1 p-6">{children}</main>
    </div>
  )
}
```

```tsx
// resources/js/Pages/Settings/Profile.tsx
import AppLayout from '@/Layouts/AppLayout'
import SettingsLayout from '@/Layouts/SettingsLayout'
import { ReactNode } from 'react'

export default function Profile() {
  return (
    <div>
      <h2>Profile Settings</h2>
      {/* Content */}
    </div>
  )
}

// Nested persistent layouts
Profile.layout = (page: ReactNode) => (
  <AppLayout>
    <SettingsLayout>{page}</SettingsLayout>
  </AppLayout>
)
```

### Conditional Layouts

```tsx
// resources/js/Pages/Login.tsx
import GuestLayout from '@/Layouts/GuestLayout'
import { ReactNode } from 'react'

export default function Login() {
  return (
    <div>
      <h1>Login</h1>
      {/* Login form */}
    </div>
  )
}

// Different layout for auth pages
Login.layout = (page: ReactNode) => <GuestLayout>{page}</GuestLayout>
```

### Layout Without Persistence

```tsx
// When you DON'T want persistence (rare)
export default function SpecialPage() {
  return <div>No layout</div>
}

// Explicitly no layout
SpecialPage.layout = (page: ReactNode) => page
```

## Benefits

- State preserved between page navigations
- Audio/video continues playing
- Form inputs in navigation preserved
- Scroll position in sidebars maintained
- Better perceived performance

```

laravel-inertia-react | SkillHub