inertia-rails/skills

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.

First seen Feb 13, 2026

Installation

$ npx skills add inertia-rails/skills --skill inertia-rails-forms

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from inertia-rails/skills · top by installs.

npx skills add inertia-rails/skills

Browse all from inertia-rails/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 67
License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,910 B
  • docs SUMMARY.md 3,856 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 441 installs

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](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](references/file-uploads.md).

Vue / Svelte

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

  • Vue 3: [references/vue.md](references/vue.md) — <Form> with #default scoped slot, useForm returns reactive proxy (form.email not setData), v-model binding
  • Svelte: [references/svelte.md](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 (redirectback, tohash, 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](references/file-uploads.md) (~200 lines) — image preview with <Form>, Active Storage integration, direct upload setup, multiple files, and progress tracking.