inertia-rails-forms

Full-stack form handling for Inertia Rails: create, edit, delete, multi-step wizard, and file upload forms with validation errors and progress tracking. React examples inline; Vue and Svelte equivalents in references. Use when building any form, handling file uploads, multi-step forms, client-side validation, or wiring form submission to Rails controllers. NEVER react-hook-form. Use `<Form>` for simple forms, useForm for dynamic/programmatic control.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "inertia-rails-forms" with this command: npx skills add inertia-rails/skills/inertia-rails-skills-inertia-rails-forms

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

PropertyTypePurpose
errorsRecord<string, string>Validation errors keyed by field name
processingbooleanTrue while request is in flight
progress{ percentage: number } | nullUpload progress (file uploads only)
hasErrorsbooleanTrue if any errors exist
wasSuccessfulbooleanTrue after last submit succeeded
recentlySuccessfulbooleanTrue for 2s after success — ideal for "Saved!" feedback
isDirtybooleanTrue if any input changed from initial value
reset(...fields) => voidReset specific fields or all fields
clearErrors(...fields) => voidClear 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

SymptomCauseFix
No access to errors/processingPlain children instead of render function<Form> children should be {({ errors, processing }) => (...)}
Form sends GET instead of POSTMissing method propAdd method="post" (or "patch", "delete")
File upload sends empty bodyPUT/PATCH with fileMultipart limitation — Inertia auto-adds _method field to convert to POST
Errors don't clear after fixing fieldStale error stateErrors auto-clear on next submit; use clearErrors('field') for immediate clearing
isDirty always falseUsing value instead of defaultValueControlled inputs (value=) bypass dirty tracking — use defaultValue
progress is always nullNo file input in formProgress tracking only activates when <Form> detects a file input
Checkbox sends "on"No explicit valueAdd value="1" to checkbox inputs
Form submits twice in devReact StrictMode double-invocationNormal in development — StrictMode remounts components. Only fires once in production
Used useForm for file upload with previewonChange + 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

inertia-rails-typescript

No summary provided by upstream source.

Repository SourceNeeds Review
General

inertia-rails-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
General

inertia-rails-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
General

inertia-rails-pages

No summary provided by upstream source.

Repository SourceNeeds Review