Help us improve
Share bugs, ideas, or general feedback.
From inertia-rails-skills
Handles full-stack forms in Inertia Rails apps: create/edit/delete, multi-step wizards, file uploads with validation errors, progress tracking using <Form> and useForm. Avoids react-hook-form.
npx claudepluginhub inertia-rails/skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/inertia-rails-skills:inertia-rails-formsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Full-stack form handling for Inertia.js + Rails.
Builds SPAs with Inertia.js and Rails using React, Vue, or Svelte. Handles Inertia pages, useForm, shared props, flash messages, and client-side routing.
Guides server-driven architecture patterns for Inertia Rails + React, with decision matrix for data loading, forms, navigation, state management. Use when building pages, CRUD, or displaying data.
Implements React forms using controlled components, React Hook Form with Zod validation, dynamic useFieldArray fields, server actions, optimistic updates, file uploads, and multi-step wizards.
Share bugs, ideas, or general feedback.
Full-stack form handling for Inertia.js + Rails.
Before building a form, ask:
<Form> component (no state management needed)<Form>.
React useState for UI state (preview URL, file size display) is independent
of form data — <Form> handles the submission; useState handles the UI.useForm hook<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:
router.get with debounce + preserveState, or raw fetch for large datasetsrouter.patch directly, or useForm if you need error display on the fieldNEVER:
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.data={...} to <Form> — it has no data prop. Data comes from input name attributes automatically. data is a useForm concept.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.value= instead of defaultValue on inputs — controlled inputs bypass <Form>'s dirty tracking, making isDirty always false.value="1" on checkboxes — without it, the browser submits "on" and Rails won't cast to boolean correctly.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>
<Form method="delete" action={`/posts/${post.id}`}>
{({ processing }) => (
<button type="submit" disabled={processing}>
{processing ? 'Deleting...' : 'Delete Post'}
</button>
)}
</Form>
| 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.
Use method="patch" and uncontrolled defaults:
defaultValuedefaultCheckeddefaultValue on <select>Checkbox without explicit
valuesubmits"on"— setvalue="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>
Use the transform prop to reshape data before submission without useForm.
For advanced transform with useForm, see references/advanced-forms.md.
formRefThe 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 HookUse 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.
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:
<Form><Form> + formRef.submit() on changeuseForm (dropped files aren't in DOM inputs, setData is cleaner)Preview / validation → useState alongside either approach, see
references/file-uploads.md.
All examples above use React syntax. For Vue 3 or Svelte equivalents:
references/vue.md — <Form> with #default scoped slot, useForm returns reactive proxy (form.email not setData), v-model bindingreferences/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.
| 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 |
inertia-rails-controllers (redirect_back, to_hash, flash)shadcn-inertia (Input/Select adaptation, toast UI)inertia-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.