inertia-rails-forms について
React、Vue、Svelte を使う Inertia Rails アプリ向けのフォーム構築ガイドです。フォーム状態、バリデーション、ファイルアップロード、Rails コントローラーの処理パターンを扱います。
主な機能
- 01リアクティブなフォーム状態管理
- 02バリデーションエラーと送信状態の処理
- 03ファイルアップロードと進捗表示
- 04ネストデータと複数フォームへの対応
- 05リアルタイム検証と楽観的更新
活用シーン
- 01ユーザー登録や編集フォームの構築
- 02サーバー側の検証エラー表示
- 03ファイルを含むフォームの送信
- 04同一ページの複数フォームでエラーを分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cole-robertson/ inertia-rails-skills --skill inertia-rails-formsインストール後、Claude Code で/inertia-rails-formsを入力して読み込まれたか確認します。
詳細
Inertia Rails のフォーム
React、Vue、Svelte を使用した Inertia Rails アプリケーションでフォームを構築するための包括的なガイドです。
useForm ヘルパー
useForm ヘルパーは、バリデーション、ファイルアップロード、送信処理の機能を備えたリアクティブなフォーム状態管理を提供します。
React
import { useForm } from '@inertiajs/react'
export default function CreateUser() {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
email: '',
password: '',
avatar: null,
})
function submit(e) {
e.preventDefault()
post('/users', {
onSuccess: () => reset('password'),
preserveScroll: true,
})
}
return (
<form onSubmit={submit}>
<div>
<label>Name</label>
<input
type="text"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<label>Email</label>
<input
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label>Password</label>
<input
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<div>
<label>Avatar</label>
<input
type="file"
onChange={(e) => setData('avatar', e.target.files[0])}
/>
</div>
<button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create User'}
</button>
</form>
)
}
Vue 3
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
name: '',
email: '',
password: '',
avatar: null,
})
function submit() {
form.post('/users', {
onSuccess: () => form.reset('password'),
preserveScroll: true,
})
}
</script>
<template>
<form @submit.prevent="submit">
<div>
<label>Name</label>
<input v-model="form.name" type="text" />
<span v-if="form.errors.name" class="error">{{ form.errors.name }}</span>
</div>
<div>
<label>Email</label>
<input v-model="form.email" type="email" />
<span v-if="form.errors.email" class="error">{{ form.errors.email }}</span>
</div>
<div>
<label>Password</label>
<input v-model="form.password" type="password" />
<span v-if="form.errors.password" class="error">{{ form.errors.password }}</span>
</div>
<div>
<label>Avatar</label>
<input type="file" @change="form.avatar = $event.target.files[0]" />
<progress v-if="form.progress" :value="form.progress.percentage" max="100" />
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Creating...' : 'Create User' }}
</button>
</form>
</template>
Svelte
<script>
import { useForm } from '@inertiajs/svelte'
const form = useForm({
name: '',
email: '',
password: '',
avatar: null,
})
function submit() {
form.post('/users', {
onSuccess: () => form.reset('password'),
preserveScroll: true,
})
}
</script>
<form onsubmit={(e) => { e.preventDefault(); submit() }}>
<div>
<label>Name</label>
<input type="text" bind:value={form.name} />
{#if form.errors.name}
<span class="error">{form.errors.name}</span>
{/if}
</div>
<div>
<label>Email</label>
<input type="email" bind:value={form.email} />
{#if form.errors.email}
<span class="error">{form.errors.email}</span>
{/if}
</div>
<div>
<label>Password</label>
<input type="password" bind:value={form.password} />
</div>
<div>
<label>Avatar</label>
<input type="file" onchange={(e) => (form.avatar = e.target.files[0])} />
</div>
<button type="submit" disabled={form.processing}>
{form.processing ? 'Creating...' : 'Create User'}
</button>
</form>
Rails コントローラーパターン
フォーム送信を処理する標準的なパターンです。
class UsersController < ApplicationController
def new
render inertia: {}
end
def create
user = User.new(user_params)
if user.save
redirect_to users_url, notice: 'User created successfully!'
else
redirect_to new_user_url, inertia: { errors: user.errors }
end
end
def edit
user = User.find(params[:id])
render inertia: { user: user.as_json(only: [:id, :name, :email]) }
end
def update
user = User.find(params[:id])
if user.update(user_params)
redirect_to user_url(user), notice: 'User updated successfully!'
else
redirect_to edit_user_url(user), inertia: { errors: user.errors }
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :avatar)
end
end
useForm のプロパティとメソッド
プロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
data | Object | 現在のフォームデータ |
errors | Object | サーバーから返されたバリデーションエラー |
hasErrors | Boolean | エラーがあるかどうか |
processing | Boolean | フォームを送信中かどうか |
progress | Object | ファイルアップロードの進捗状況 |
wasSuccessful | Boolean | 送信が成功した後に true になります |
recentlySuccessful | Boolean | 成功後 2 秒間 true になります |
isDirty | Boolean | フォームデータが変更されているかどうか |
メソッド
| メソッド | 説明 |
|---|---|
setData(key, value) | 単一のフィールド値を設定します |
setData(values) | 複数のフィールド値を設定します |
reset() | すべてのフィールドを初期値に戻します |
reset(...fields) | 指定したフィールドをリセットします |
clearErrors() | すべてのバリデーションエラーを消去します |
clearErrors(...fields) | 指定したフィールドのエラーを消去します |
setError(field, message) | カスタムエラーを設定します |
setError(errors) | 複数のエラーを設定します |
transform(callback) | 送信前にデータを変換します |
defaults() | リセット時のデフォルト値を更新します |
get(url, options) | GET リクエストを送信します |
post(url, options) | POST リクエストを送信します |
put(url, options) | PUT リクエストを送信します |
patch(url, options) | PATCH リクエストを送信します |
delete(url, options) | DELETE リクエストを送信します |
cancel() | 進行中のフォーム送信をキャンセルします |
送信オプション
form.post('/users', {
// Preserve component state on validation errors
preserveState: true, // or 'errors' to preserve only on errors
// Preserve scroll position
preserveScroll: true, // or 'errors' to preserve only on errors
// Custom headers
headers: { 'X-Custom': 'value' },
// Force FormData even without files
forceFormData: true,
// Error bag for multiple forms on same page
errorBag: 'createUser',
// Event callbacks
onBefore: (visit) => confirm('Submit form?'),
onStart: (visit) => {},
onProgress: (progress) => {},
onSuccess: (page) => form.reset(),
onError: (errors) => console.log(errors),
onCancel: () => {},
onFinish: () => {},
})
ファイルアップロード
Inertia は、ファイルを含むフォームを自動的に FormData に変換します。
const form = useForm({
name: '',
avatar: null,
documents: [], // Multiple files
})
// Single file
<input type="file" onChange={(e) => setData('avatar', e.target.files[0])} />
// Multiple files
<input
type="file"
multiple
onChange={(e) => setData('documents', Array.from(e.target.files))}
/>
アップロードの進捗状況
<template>
<div v-if="form.progress">
<progress :value="form.progress.percentage" max="100" />
<span>{{ form.progress.percentage }}%</span>
</div>
</template>
PUT/PATCH を使ったファイルアップロード
一部のサーバーは multipart PUT/PATCH をサポートしていません。メソッド偽装を使用してください。
// Instead of form.put()
form.post(`/users/${user.id}`, {
_method: 'put', // Rails recognizes this
})
ネストされたデータ
ネストされた属性にはブラケット記法を使用します。
const form = useForm({
user: {
name: '',
profile: {
bio: '',
},
},
})
// Access errors
form.errors['user.name']
form.errors['user.profile.bio']
または、Rails 形式のパラメーターを使用します。
<input name="user[name]" v-model="form.user.name" />
<input name="user[profile][bio]" v-model="form.user.profile.bio" />
同じページに複数のフォームを配置する場合
エラーバッグを使用してバリデーションエラーを分離します。
// Login form
const loginForm = useForm({ email: '', password: '' })
loginForm.post('/login', { errorBag: 'login' })
// Register form
const registerForm = useForm({ name: '', email: '', password: '' })
registerForm.post('/register', { errorBag: 'register' })
サーバー側:
def create
# ...
redirect_to root_url, inertia: {
errors: { login: { email: 'Invalid credentials' } }
}
end
エラーには page.props.errors.login.email でアクセスします。
フォームの変換
送信前にデータを変換します。
const form = useForm({
first_name: 'John',
last_name: 'Doe',
})
form
.transform((data) => ({
...data,
full_name: `${data.first_name} ${data.last_name}`,
}))
.post('/users')
Form コンポーネント(宣言的)
よりシンプルなフォームには、Form コンポーネントを使用します。
Vue
<script setup>
import { Form } from '@inertiajs/vue3'
</script>
<template>
<Form action="/users" method="post" v-slot="{ errors, processing }">
<input type="text" name="name" />
<span v-if="errors.name">{{ errors.name }}</span>
<input type="email" name="email" />
<span v-if="errors.email">{{ errors.email }}</span>
<button type="submit" :disabled="processing">
Submit
</button>
</Form>
</template>
React
import { Form } from '@inertiajs/react'
export default function CreateUser() {
return (
<Form action="/users" method="post">
{({ errors, processing }) => (
<>
<input type="text" name="name" />
{errors.name && <span>{errors.name}</span>}
<input type="email" name="email" />
{errors.email && <span>{errors.email}</span>}
<button type="submit" disabled={processing}>
Submit
</button>
</>
)}
</Form>
)
}
Form コンポーネントの高度な機能(v3)
v3 の Form コンポーネントは、ネストされたデータや配列に対応したドット記法をサポートしています。
import { Form } from '@inertiajs/react'
export default function CreateReport() {
return (
<Form action="/reports" method="post">
{({ errors, processing, isDirty, wasSuccessful }) => (
<>
{/* Dotted notation for nested data */}
<input type="text" name="report.title" />
{errors['report.title'] && <span>{errors['report.title']}</span>}
<input type="text" name="report.metadata.author" />
{/* Array support */}
<input type="text" name="tags[]" />
<input type="text" name="tags[]" />
{/* Disable inputs while processing */}
<button type="submit" disabled={processing}>
Submit
</button>
</>
)}
</Form>
)
}
useFormContext フック
プロパティの受け渡しを繰り返すことなく、深くネストされたコンポーネントからフォームの状態にアクセスします。
import { useFormContext } from '@inertiajs/react'
function SubmitButton() {
const { processing, isDirty } = useFormContext()
return (
<button type="submit" disabled={processing || !isDirty}>
{processing ? 'Saving...' : 'Save'}
</button>
)
}
Precognition(リアルタイムバリデーション)
クライアント側でバリデーションルールを重複させることなく、サーバー側でリアルタイムにフォームを検証します。
import { Form } from '@inertiajs/react'
export default function CreateUser() {
return (
<Form action="/users" method="post" precognition>
{({ errors, valid, invalid, validate }) => (
<>
<input
type="email"
name="email"
onBlur={() => validate('email')}
/>
{valid('email') && <span className="text-green-500">✓</span>}
{invalid('email') && <span className="text-red-500">{errors.email}</span>}
<button type="submit">Create</button>
</>
)}
</Form>
)
}
サーバー側:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.precognition_prevent_writes = true # Prevent DB writes during validation
end
フォームの状態を記憶する
useRemember を使用して、ブラウザーの履歴を移動したときもフォームデータを保持します。
useRemember フック
import { useRemember } from '@inertiajs/vue3'
// Form state persists across back/forward navigation
const form = useRemember({
name: '',
email: '',
message: '',
})
同じページ上の複数のコンポーネント
複数のコンポーネントで remember を使用する場合は、一意のキーを指定してください。
// Contact form
const contactForm = useRemember({
email: '',
message: '',
}, 'ContactForm')
// Newsletter form
const newsletterForm = useRemember({
email: '',
}, 'NewsletterForm')
useForm ヘルパーとの併用
フォームヘルパーには、remember のサポートが組み込まれています。
// Pass a unique key as first argument
const form = useForm('CreateUser', {
name: '',
email: '',
password: '',
})
// For edit forms, include the ID for uniqueness
const form = useForm(`EditUser:${props.user.id}`, {
name: props.user.name,
email: props.user.email,
})
手動での状態管理
import { router } from '@inertiajs/vue3'
// Save state manually
router.remember({ step: 2, selections: ['a', 'b'] }, 'wizard-state')
// Restore state
const savedState = router.restore('wizard-state')
if (savedState) {
// Restore component state from savedState
}
React の例
import { useRemember } from '@inertiajs/react'
export default function ContactForm() {
const [form, setForm] = useRemember({
name: '',
email: '',
message: '',
}, 'ContactForm')
return (
<form>
<input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
{/* ... */}
</form>
)
}
useHttp フック(v3)
Inertia のページ遷移を発生させずに、単独の HTTP リクエストを実行します。ナビゲーションを必要としない API 呼び出し、バックグラウンド処理、AJAX リクエストに便利です。
React
import { useHttp } from '@inertiajs/react'
export default function SearchUsers() {
const { data, post, processing, errors } = useHttp()
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
function search() {
post('/api/search', {
data: { query },
onSuccess: (response) => setResults(response.data),
})
}
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={search} disabled={processing}>
{processing ? 'Searching...' : 'Search'}
</button>
{results.map(user => <div key={user.id}>{user.name}</div>)}
</div>
)
}
useForm との主な違い
| 機能 | useForm | useHttp |
|---|---|---|
| ナビゲーションをトリガーする | はい | いいえ |
| Promise を返す | いいえ | はい |
| ページのプロパティを更新する | はい | いいえ |
| リアクティブな状態 | はい | はい |
| ファイルアップロードの進行状況 | はい | はい |
| Precognition のサポート | はい | はい |
ページ遷移を発生させずに API リクエストを行う必要がある場合は、useHttp を使用してください。
楽観的更新(v3)
サーバーからの応答前に UI をすぐに更新し、失敗時は自動的にロールバックします。
router を使用する場合
import { router } from '@inertiajs/react'
function toggleLike(post) {
router
.optimistic((page) => {
// Optimistically update the page props
const postIndex = page.props.posts.findIndex(p => p.id === post.id)
page.props.posts[postIndex].liked = !post.liked
page.props.posts[postIndex].likes_count += post.liked ? -1 : 1
})
.post(`/posts/${post.id}/toggle-like`)
}
Form コンポーネントを使用する場合
<Form
action={`/posts/${post.id}/toggle-like`}
method="post"
optimistic={(page) => {
const p = page.props.posts.find(p => p.id === post.id)
p.liked = !p.liked
}}
>
<button type="submit">
{post.liked ? '❤️' : '🤍'} {post.likes_count}
</button>
</Form>
useForm を使用する場合
const form = useForm({})
form
.optimistic((page) => {
page.props.posts[0].title = 'Updated title'
})
.patch(`/posts/${post.id}`)
サーバーがバリデーションエラーまたは 2xx 以外のレスポンスを返した場合、楽観的更新は自動的にロールバックされます。
ベストプラクティス
1. 常に PRG パターンを使用する
# Incorrect - renders on POST
def create
@user = User.create(user_params)
render inertia: { user: @user }
end
# Correct - redirect after action
def create
user = User.create(user_params)
redirect_to user_url(user)
end
2. エラーデータは最小限にする
# Only include field errors, not full model
redirect_to new_user_url, inertia: { errors: user.errors.to_hash }
3. クライアントとサーバーの両方でバリデーションを行う
// Client-side for UX
const validateEmail = (email) => {
if (!email.includes('@')) {
form.setError('email', 'Invalid email format')
return false
}
return true
}
function submit() {
if (validateEmail(form.email)) {
form.post('/users') // Server validates too
}
}
4. エラー時はスクロール位置を保持する
form.post('/users', {
preserveScroll: 'errors', // Only preserve on validation errors
})
5. 成功時に機密フィールドをリセットする
form.post('/users', {
onSuccess: () => form.reset('password', 'password_confirmation'),
})
6. 読み込み状態を表示する
<button type="submit" :disabled="form.processing">
<span v-if="form.processing">
<Spinner /> Saving...
</span>
<span v-else>Save</span>
</button>
7. 破壊的な操作は確認する
function deleteUser() {
if (confirm('Are you sure?')) {
router.delete(`/users/${user.id}`)
}
}