laravel-inertia-react について
Laravel、Inertia.js、React でフルスタックアプリを開発する人向けのリファレンススキルです。ページ、フォーム、ナビゲーション、共有データ、レイアウト、アップロードのルールと例をまとめています。
主な機能
- 01型付き Inertia ページコンポーネント
- 02useForm によるフォーム処理
- 03ナビゲーションと部分再読み込み
- 04共有データと認証
- 05永続レイアウトとネストレイアウト
- 06ファイルアップロードのパターン
活用シーン
- 01React で Inertia ページを構築
- 02Laravel のフォーム検証エラーを処理
- 03認証済みユーザーデータを共有
- 04進捗表示付きファイルアップロードの追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ asyrafhussin/ agent-skills --skill laravel-inertia-reactインストール後、Claude Code で/laravel-inertia-reactを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonREADME.mdrules/_sections.md_template.mdform-dirty-tracking.mdform-file-uploads.mdform-progress-indicator.mdform-reset-handling.mdform-transform-data.mdform-useform-basic.mdform-useform-hook.mdform-validation-errors.mdlayout-persistent.mdnav-external-links.mdnav-link-component.mdnav-preserve-state.mdnav-programmatic.mdnav-replace-history.mdpage-component-structure.mdpage-default-layout.mdpage-head-management.mdpage-partial-reloads.mdpage-props-typing.mdpage-scroll-preservation.mdshared-app-config.mdshared-auth-user.mdshared-flash-messages.mdshared-ziggy-routes.md詳細
Laravel + Inertia.js + React
Laravel、Inertia.js、React を使用して最新のモノリシックアプリケーションを構築するための包括的なパターンです。シームレスなフルスタック開発のための 30 以上のルールが含まれています。
適用するタイミング
以下の場合に、このガイドラインを参照してください。
- Inertia のページコンポーネントを作成する場合
- useForm フックを使ってフォームを処理する場合
- 共有データと認証を管理する場合
- 永続レイアウトを実装する場合
- ページ間を移動する場合
優先度別のルールカテゴリー
| 優先度 | カテゴリー | 影響度 | プレフィックス |
|---|---|---|---|
| 1 | ページコンポーネント | CRITICAL | page- |
| 2 | フォームとバリデーション | CRITICAL | form- |
| 3 | ナビゲーションとリンク | HIGH | nav- |
| 4 | 共有データ | HIGH | shared- |
| 5 | レイアウト | MEDIUM | layout- |
| 6 | ファイルアップロード | MEDIUM | upload- |
| 7 | 高度なパターン | LOW | advanced- |
クイックリファレンス
1. ページコンポーネント(CRITICAL)
page-props-typing- Laravel からページの props の型を定義するpage-component-structure- 標準的なページコンポーネントのパターンpage-head-management- Head を使ったタイトルとメタタグの管理page-default-layout- ページにレイアウトを割り当てる
2. フォームとバリデーション(CRITICAL)
form-useform-basic- useForm の基本的な使い方form-validation-errors- Laravel のバリデーションエラーを表示するform-processing-state- フォーム送信の状態を処理するform-reset-preserve- フォームデータをリセットするか保持するform-nested-data- ネストされたフォームデータを処理するform-transform- 送信前にデータを変換する
3. ナビゲーションとリンク(HIGH)
nav-link-component- ナビゲーションに Link を使うnav-preserve-state- スクロール位置と状態を保持するnav-partial-reloads- 変更された部分のみを再読み込みするnav-replace-history- 履歴を置き換えるか追加する
4. 共有データ(HIGH)
shared-auth-user- 認証済みユーザーにアクセスするshared-flash-messages- フラッシュメッセージを処理するshared-global-props- グローバル props にアクセスするshared-typescript- 共有データの型を定義する
5. レイアウト(MEDIUM)
layout-persistent- 永続レイアウトのパターンlayout-nested- ネストされたレイアウトlayout-default- デフォルトレイアウトの割り当てlayout-conditional- 条件付きレイアウト
6. ファイルアップロード(MEDIUM)
upload-basic- ファイルアップロードの基本upload-progress- アップロードの進行状況を追跡するupload-multiple- 複数ファイルのアップロード
7. 高度なパターン(LOW)
advanced-polling- リアルタイムポーリングadvanced-prefetch- ページを事前取得するadvanced-modal-pages- モーダルをページとして扱うadvanced-infinite-scroll- 無限スクロール
重要なパターン
TypeScript を使ったページコンポーネント
// 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>
</>
)
}
useForm を使ったフォーム
// 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>
Laravel コントローラー
<?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'),
]);
}
}
共有データ(HandleInertiaRequests)
<?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>
)
}
使い方
詳しい説明とコード例については、各ルールのファイルを参照してください。
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)
参考資料
- Inertia.js ドキュメント - Inertia.js の公式ドキュメント
- Laravel ドキュメント - Laravel フレームワークのドキュメント
- React ドキュメント - React の公式ドキュメント
- Ziggy - JavaScript 用の Laravel ルートヘルパー
ライセンス
MIT ライセンス。このスキルは、教育および開発目的で現状のまま提供されます。
メタデータ
- バージョン: 1.0.1
- 最終更新日: 2026-01-17
- メンテナー: Asyraf Hussin
- ルール数: 6 カテゴリーにわたる 24 ルール
- 技術スタック: Laravel 10+、Inertia.js 1.0+、React 18+、TypeScript 5+
コメント
Markdown 対応