mcpField
I
inertia-rails-forms

inertia-rails-forms

github.com/cole-robertson開発3888 回インストール32 週間前に更新
リポジトリ

inertia-rails-forms について

React、Vue、Svelte を使う Inertia Rails アプリ向けのフォーム構築ガイドです。フォーム状態、バリデーション、ファイルアップロード、Rails コントローラーの処理パターンを扱います。

主な機能

  1. 01リアクティブなフォーム状態管理
  2. 02バリデーションエラーと送信状態の処理
  3. 03ファイルアップロードと進捗表示
  4. 04ネストデータと複数フォームへの対応
  5. 05リアルタイム検証と楽観的更新

活用シーン

  1. 01ユーザー登録や編集フォームの構築
  2. 02サーバー側の検証エラー表示
  3. 03ファイルを含むフォームの送信
  4. 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 のプロパティとメソッド

プロパティ
プロパティ型説明
dataObject現在のフォームデータ
errorsObjectサーバーから返されたバリデーションエラー
hasErrorsBooleanエラーがあるかどうか
processingBooleanフォームを送信中かどうか
progressObjectファイルアップロードの進捗状況
wasSuccessfulBoolean送信が成功した後に true になります
recentlySuccessfulBoolean成功後 2 秒間 true になります
isDirtyBooleanフォームデータが変更されているかどうか
メソッド
メソッド説明
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 との主な違い
機能useFormuseHttp
ナビゲーションをトリガーするはいいいえ
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}`)
  }
}

コメント