skills/inertia-rails/skills/inertia-rails-forms

inertia-rails-forms

SKILL.md

Inertia Rails Forms

Full-stack form handling for Inertia.js + Rails.

Before building a form, ask:

  • Simple create/edit?<Form> component (no state management needed)
  • Requires per-field UI elements? → Still <Form>. React useState for UI state (preview URL, file size display) is independent of form data — <Form> handles the submission; useState handles the UI.
  • Multi-step wizard, dynamic fields (add/remove inputs), or form data shared with sibling components (e.g., live preview panel)?useForm hook
  • Tempted by react-hook-form? → Don't. Inertia's <Form> already handles CSRF tokens, redirect following, error mapping from Rails, processing state, file upload detection, and history state. react-hook-form would duplicate or fight all of this.

When NOT to use <Form> or useForm:

  • Data lookups — not a form submission. Use router.get with debounce + preserveState, or raw fetch for large datasets
  • Inline single-field edits without navigationrouter.patch directly, or useForm if you need error display on the field

NEVER:

  • Use react-hook-form, vee-validate, or sveltekit-superforms — Inertia <Form> already handles CSRF, redirect following, error mapping, processing state, and file detection. These libraries fight Inertia's request lifecycle.
  • Pass data={...} to <Form> — it has no data prop. Data comes from input name attributes automatically. data is a useForm concept.
  • Use useForm for simple create/edit — <Form> handles these without state management. Reserve useForm for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.
  • Use controlled value= instead of defaultValue on inputs — controlled inputs bypass <Form>'s dirty tracking, making isDirty always false.
  • Omit value="1" on checkboxes — without it, the browser submits "on" and Rails won't cast to boolean correctly.
  • Call useForm inside a loop or conditional — it's a hook (React rules apply). Create one form instance per logical form.

<Form> Component (Preferred)

The simplest way to handle forms. Collects data from input name attributes automatically — no manual state management needed. <Form> has NO data prop — do NOT pass data={...} (that's a useForm concept). For edit forms, use defaultValue on inputs.

Use render function children {({ errors, processing }) => (...)} to access form state. Plain children work but give no access to errors, processing, or progress.

import { Form } from '@inertiajs/react'

export default function CreateUser() {
  return (
    <Form method="post" action="/users">
      {({ errors, processing }) => (
        <>
          <input type="text" name="name" />
          {errors.name && <span className="error">{errors.name}</span>}

          <input type="email" name="email" />
          {errors.email && <span className="error">{errors.email}</span>}

          <button type="submit" disabled={processing}>
            {processing ? 'Creating...' : 'Create User'}
          </button>
        </>
      )}
    </Form>
  )
}

// Plain children — valid but no access to errors/processing/progress:
// <Form method="post" action="/users">
//   <input name="name" />
//   <button type="submit">Create</button>
// </Form>

Delete Form

<Form method="delete" action={`/posts/${post.id}`}>
  {({ processing }) => (
    <button type="submit" disabled={processing}>
      {processing ? 'Deleting...' : 'Delete Post'}
    </button>
  )}
</Form>

Key Render Function Properties

Property Type Purpose
errors Record<string, string> Validation errors keyed by field name
processing boolean True while request is in flight
progress { percentage: number } | null Upload progress (file uploads only)
hasErrors boolean True if any errors exist
wasSuccessful boolean True after last submit succeeded
recentlySuccessful boolean True for 2s after success — ideal for "Saved!" feedback
isDirty boolean True if any input changed from initial value
reset (...fields) => void Reset specific fields or all fields
clearErrors (...fields) => void Clear specific errors or all errors

Additional <Form> props (errorBag, only, resetOnSuccess, event callbacks like onBefore, onSuccess, onError, onProgress) are documented in references/advanced-forms.md — see loading trigger below.

Edit Form (Pre-populated)

Use method="patch" and uncontrolled defaults:

  • Text/textarea → defaultValue
  • Checkbox/radio → defaultChecked
  • Select → defaultValue on <select>

Checkbox without explicit value submits "on" — set value="1" so Rails casts to boolean correctly.

<Form method="patch" action={`/posts/${post.id}`}>
  {({ errors, processing }) => (
    <>
      <input type="text" name="title" defaultValue={post.title} />
      {errors.title && <span className="error">{errors.title}</span>}

      <label>
        <input type="checkbox" name="published" value="1"
          defaultChecked={post.published} />
        Published
      </label>

      <button type="submit" disabled={processing}>
        {processing ? 'Saving...' : 'Update Post'}
      </button>
    </>
  )}
</Form>

Transforming Data

Use the transform prop to reshape data before submission without useForm.

For advanced transform with useForm, see references/advanced-forms.md.

External Access with formRef

The ref exposes the same methods and state as render function props (FormComponentSlotProps). Use when you need to interact with the form from outside <Form>. Key ref methods: submit(), reset(), clearErrors(), setError(), getData(), getFormData(), validate(), touch(), defaults(). State: errors, processing, progress, isDirty, hasErrors, wasSuccessful, recentlySuccessful.

import {useRef} from 'react'
import {Form} from '@inertiajs/react'
import type {FormComponentRef} from '@inertiajs/core'

export default function CreateUser() {
  const formRef = useRef<FormComponentRef>(null)

  return (
    <>
      <Form ref={formRef} method='post' action='/users'>
        {({errors}) => (
          <>
            <input type='text' name='name'/>
            {errors.name && <span className='error'>{errors.name}</span>}
          </>
        )}
      </Form>
      <button onClick={() => formRef.current?.submit()}>Submit</button>
      <button onClick={() => formRef.current?.reset()}>Reset</button>
    </>
  )
}

useForm Hook

Use useForm only for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.

MANDATORY — READ ENTIRE FILE when using useForm hook, transform, errorBag, resetOnSuccess, multi-step forms, or client-side validation with setError: references/advanced-forms.md (~330 lines) — full useForm API, transform examples, error bag scoping, multi-step wizard patterns, and client-side validation.

Do NOT load advanced-forms.md when using <Form> component for simple create/edit forms — the examples above are sufficient.

File Uploads

Both <Form> and useForm auto-detect files and switch to FormData. Upload progress is built into the render function — destructure progress alongside errors and processing:

type Props = { user: User }

export default function EditProfile({ user }: Props) {
  return (
    <Form method="patch" action="/profile">
      {({ errors, processing, progress }) => (
        <>
          <input type="text" name="name" defaultValue={user.name} />
          {errors.name && <span className="error">{errors.name}</span>}

          <input type="file" name="avatar" />
          {errors.avatar && <span className="error">{errors.avatar}</span>}

          {progress && (
            <progress value={progress.percentage ?? 0} max="100" />
          )}

          <button type="submit" disabled={processing}>
            {processing ? 'Uploading...' : 'Save'}
          </button>
        </>
      )}
    </Form>
  )
}

Choosing <Form> vs useForm for uploads:

  • File submits with other fields (avatar + name, one Save button) → file input inside <Form>
  • Standalone immediate upload (uploads on select, no Save button) → <Form> + formRef.submit() on change
  • Drag-and-drop uploaduseForm (dropped files aren't in DOM inputs, setData is cleaner)

Preview / validation → useState alongside either approach, see references/file-uploads.md.

Vue / Svelte

All examples above use React syntax. For Vue 3 or Svelte equivalents:

  • Vue 3: references/vue.md<Form> with #default scoped slot, useForm returns reactive proxy (form.email not setData), v-model binding
  • Svelte: references/svelte.md<Form> with {#snippet} syntax, useForm returns Writable store ($form.email), bind:value, ref exposes methods only (not reactive state)

MANDATORY — READ THE MATCHING FILE when the project uses Vue or Svelte.

Troubleshooting

Symptom Cause Fix
No access to errors/processing Plain children instead of render function <Form> children should be {({ errors, processing }) => (...)}
Form sends GET instead of POST Missing method prop Add method="post" (or "patch", "delete")
File upload sends empty body PUT/PATCH with file Multipart limitation — Inertia auto-adds _method field to convert to POST
Errors don't clear after fixing field Stale error state Errors auto-clear on next submit; use clearErrors('field') for immediate clearing
isDirty always false Using value instead of defaultValue Controlled inputs (value=) bypass dirty tracking — use defaultValue
progress is always null No file input in form Progress tracking only activates when <Form> detects a file input
Checkbox sends "on" No explicit value Add value="1" to checkbox inputs
Form submits twice in dev React StrictMode double-invocation Normal in development — StrictMode remounts components. Only fires once in production
Used useForm for file upload with preview onChange + useState mistaken for "programmatic data manipulation" <Form> + useState for preview UI. useForm is only needed when form submission data must live in React state (multi-step, dynamic add/remove fields). File preview is local UI state, not form data

Related Skills

  • Server-side PRG & errorsinertia-rails-controllers (redirect_back, to_hash, flash)
  • shadcn inputsshadcn-inertia (Input/Select adaptation, toast UI)
  • Page props typinginertia-rails-typescript (type Props not interface, TS2344)

MANDATORY — READ ENTIRE FILE when handling file uploads with image preview, Active Storage, or direct uploads: references/file-uploads.md (~200 lines) — image preview with <Form>, Active Storage integration, direct upload setup, multiple files, and progress tracking.

Weekly Installs
54
GitHub Stars
34
First Seen
Feb 13, 2026
Installed on
gemini-cli53
opencode53
codex53
github-copilot52
amp51
kimi-cli50