laravel-inertia-react

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Laravel + Inertia.js + React

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.
这是使用Laravel、Inertia.js和React构建现代单体应用的综合模式指南,包含30+条实现无缝全栈开发的规则。

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
在以下场景中可参考本指南:
  • 构建Inertia页面组件
  • 使用useForm钩子处理表单
  • 管理共享数据与认证信息
  • 实现持久化布局
  • 页面间导航

Rule Categories by Priority

按优先级划分的规则类别

PriorityCategoryImpactPrefix
1Page ComponentsCRITICAL
page-
2Forms & ValidationCRITICAL
form-
3Navigation & LinksHIGH
nav-
4Shared DataHIGH
shared-
5LayoutsMEDIUM
layout-
6File UploadsMEDIUM
upload-
7Advanced PatternsLOW
advanced-
优先级类别影响程度前缀
1页面组件关键
page-
2表单与验证关键
form-
3导航与链接
nav-
4共享数据
shared-
5布局
layout-
6文件上传
upload-
7高级模式
advanced-

Quick Reference

快速参考

1. Page Components (CRITICAL)

1. 页面组件(关键)

  • 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
  • page-props-typing
    - 为来自Laravel的页面props添加类型定义
  • page-component-structure
    - 标准页面组件模式
  • page-head-management
    - 使用Head组件管理标题与元标签
  • page-default-layout
    - 为页面分配布局

2. Forms & Validation (CRITICAL)

2. 表单与验证(关键)

  • 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
  • form-useform-basic
    - useForm基础用法
  • form-validation-errors
    - 展示Laravel验证错误信息
  • form-processing-state
    - 处理表单提交状态
  • form-reset-preserve
    - 表单数据重置与保留的处理
  • form-nested-data
    - 处理嵌套表单数据
  • form-transform
    - 提交前转换数据

3. Navigation & Links (HIGH)

3. 导航与链接(高)

  • 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
  • nav-link-component
    - 使用Link组件实现导航
  • nav-preserve-state
    - 保留滚动位置与页面状态
  • nav-partial-reloads
    - 仅重新加载变更内容
  • nav-replace-history
    - 替换历史记录与新增历史记录的区别

4. Shared Data (HIGH)

4. 共享数据(高)

  • shared-auth-user
    - Access authenticated user
  • shared-flash-messages
    - Handle flash messages
  • shared-global-props
    - Access global props
  • shared-typescript
    - Type shared data
  • shared-auth-user
    - 访问已认证用户信息
  • shared-flash-messages
    - 处理闪存消息
  • shared-global-props
    - 访问全局props
  • shared-typescript
    - 为共享数据添加类型定义

5. Layouts (MEDIUM)

5. 布局(中)

  • layout-persistent
    - Persistent layouts pattern
  • layout-nested
    - Nested layouts
  • layout-default
    - Default layout assignment
  • layout-conditional
    - Conditional layouts
  • layout-persistent
    - 持久化布局模式
  • layout-nested
    - 嵌套布局
  • layout-default
    - 默认布局分配
  • layout-conditional
    - 条件式布局

6. File Uploads (MEDIUM)

6. 文件上传(中)

  • upload-basic
    - Basic file upload
  • upload-progress
    - Upload progress tracking
  • upload-multiple
    - Multiple file uploads
  • upload-basic
    - 基础文件上传
  • upload-progress
    - 上传进度追踪
  • upload-multiple
    - 多文件上传

7. Advanced Patterns (LOW)

7. 高级模式(低)

  • advanced-polling
    - Real-time polling
  • advanced-prefetch
    - Prefetch pages
  • advanced-modal-pages
    - Modal as pages
  • advanced-infinite-scroll
    - Infinite scrolling
  • advanced-polling
    - 实时轮询
  • advanced-prefetch
    - 页面预加载
  • advanced-modal-pages
    - 将模态框作为页面实现
  • advanced-infinite-scroll
    - 无限滚动

Essential Patterns

核心模式示例

Page Component with TypeScript

带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>
    </>
  )
}
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

使用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>
    </>
  )
}
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>
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

Laravel控制器

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

共享数据配置(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'),
            ],
        ]);
    }
}
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>
  )
}
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
阅读单个规则文件获取详细说明和代码示例:
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)
laravel-inertia-react/
├── SKILL.md                 # 本文档 - 概览与示例
├── README.md                # 快速参考指南
├── AGENTS.md                # AI Agent集成指南
├── metadata.json            # 技能元数据与参考链接
└── rules/
    ├── _sections.md         # 规则类别与优先级
    ├── _template.md         # 新规则模板
    ├── page-*.md            # 页面组件模式(6条规则)
    ├── form-*.md            # 表单处理模式(8条规则)
    ├── nav-*.md             # 导航模式(5条规则)
    ├── shared-*.md          # 共享数据模式(4条规则)
    └── layout-*.md          # 布局模式(1条规则)

References

参考链接

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.
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+
  • 版本: 1.0.0
  • 最后更新: 2026-01-17
  • 维护者: Asyraf Hussin
  • 规则数量: 6个类别共24条规则
  • 技术栈: Laravel 10+, Inertia.js 1.0+, React 18+, TypeScript 5+