inertia-rails-forms — Claude Code skill: Inertia Rails forms
A skill for building forms in Inertia.js + Rails: create, edit, delete, multi-step wizards and file uploads. Useful when wiring form submission to Rails controllers.
npx skills add inertia-rails/skills --skill inertia-rails-forms -a claude-codeWhat inertia-rails-forms does
The skill covers the full form workflow in Inertia Rails. React examples sit inline in the text, with Vue and Svelte equivalents in references. It walks through create, edit and delete forms, multi-step wizards, and file uploads with progress tracking.
The main tool is the <Form> component from @inertiajs/react. It collects data from name attributes on its own and handles CSRF tokens, redirect following, error mapping from Rails, and processing state. Simple forms need no state management at all. The useForm hook is reserved for wizards, dynamic fields, and cases where sibling components need the form data.
The skill is strict about what not to do. react-hook-form, vee-validate and sveltekit-superforms are off the table because they fight Inertia's request lifecycle. Passing data={...} to <Form> is wrong — there is no such prop. Controlled value= instead of defaultValue breaks dirty tracking, so isDirty stays false. A checkbox without value="1" submits "on", and Rails won't cast that to boolean. Worth checking your own forms if validation has been behaving oddly.
What it helps with
- build a create and edit form without manual state management
- show Rails validation errors next to the right fields
- run a multi-step wizard with shared form state
- upload a file and display progress
- add and remove form fields dynamically
Example prompts for the agent
- Build a user creation form in Inertia Rails with the
<Form>component and error display - I need a multi-step wizard using
useFormwith data shared across steps - Add file upload with progress to the post edit form
How to install inertia-rails-forms
1Skills CLI (Vercel)
npx skills add inertia-rails/skills --skill inertia-rails-forms -a claude-code2Manually: put the files in place
~/.claude/skills/inertia-rails-forms/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/inertia-rails-forms/
1Skills CLI (Vercel)
npx skills add inertia-rails/skills --skill inertia-rails-forms -a codex2Manually: put the files in place
.agents/skills/inertia-rails-forms/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add inertia-rails/skills --skill inertia-rails-forms -a cursor2Manually: put the files in place
.cursor/skills/inertia-rails-forms/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install inertia-rails/skills inertia-rails-forms2Manually: put the files in place
.github/skills/inertia-rails-forms/SKILL.md1Skills CLI (Vercel)
npx skills add inertia-rails/skills --skill inertia-rails-forms -a gemini-cli2Platform CLI
gemini skills install https://github.com/inertia-rails/skills --path skills/inertia-rails-formsManually: put the files in place
.devin/skills/inertia-rails-forms/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add inertia-rails/skills --skill inertia-rails-forms -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about inertia-rails-forms
How do I install inertia-rails-forms in Claude?
Run npx skills add inertia-rails/skills --skill inertia-rails-forms -a claude-code. The skill lands at ~/.claude/skills/inertia-rails-forms/SKILL.md.
What does inertia-rails-forms do?
It guides the agent through writing Inertia.js + Rails forms: create, edit, delete, wizards and file uploads. It shows how to wire submission to Rails controllers and render validation errors.
Can I use react-hook-form with this skill?
No. The skill explicitly rules out react-hook-form, vee-validate and sveltekit-superforms because they duplicate what <Form> already does and clash with Inertia's request lifecycle.
Source text
SKILL.md · MIT
---
name: inertia-rails-forms
description: >-
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.
---
# 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 navigation** – `router.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.
```tsx
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
```tsx
<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.
```tsx
<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`.
```tsx
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`:
```tsx
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 upload** → `useForm` (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 & errors** → `inertia-rails-controllers` (redirect_back, to_hash, flash)
- **shadcn inputs** → `shadcn-inertia` (Input/Select adaptation, toast UI)
- **Page props typing** → `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`](references/file-uploads.md) (~200 lines) — image preview
with `<Form>`, Active Storage integration, direct upload setup, multiple files,
and progress tracking.
© inertia-rails · MIT · inertia-rails/skills