--- id: inertia-rails/skills/inertia-rails-forms version: "cf037f0a" license: MIT install: manual updated: 2026-02-13 --- # inertia-rails-forms — Build forms in Inertia.js + Rails using the `
` component for simple create/edit workflows or `useForm` for multi-step wizards and dynamic fields. Automatic CSRF token handling, error mapping, file upload detection, and progress tracking eliminate the need for external form libraries. React, Vue, and Svelte examples included. Publisher: inertia-rails · Stars: 64 · Updated: 2026-02-13 Install (manual): `git clone https://github.com/inertia-rails/skills` ## SKILL.md # Inertia Rails Forms Full-stack form handling for Inertia.js + Rails. **Before building a form, ask:** - **Simple create/edit?** → `` component (no state management needed) - **Requires per-field UI elements?** → Still ``. React `useState` for UI state (preview URL, file size display) is independent of form data — `` 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 `` 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 `` 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 `` already handles CSRF, redirect following, error mapping, processing state, and file detection. These libraries fight Inertia's request lifecycle. - Pass `data={...}` to `` — it has no `data` prop. Data comes from input `name` attributes automatically. `data` is a `useForm` concept. - Use `useForm` for simple create/edit — `` 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 ``'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. ## `` Component (Preferred) The simplest way to handle forms. Collects data from input `name` attributes automatically — no manual state management needed. `` 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 ( {({ errors, processing }) => ( <> {errors.name && {errors.name}} {errors.email && {errors.email}} )}
) } // Plain children — valid but no access to errors/processing/progress: //
// // //
``` ### Delete Form ```tsx
{({ processing }) => ( )}
``` ### Key Render Function Properties | Property | Type | Purpose | |----------|------|---------| | `errors` | `Record` | 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 `
` 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 ` {errors.title && {errors.title}} )}
``` ### 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 `
`. 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(null) return ( <> {({errors}) => ( <> {errors.name && {errors.name}} )} ) } ``` ## `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 `
` component for simple create/edit forms — the examples above are sufficient. ## File Uploads Both `` 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 ( {({ errors, processing, progress }) => ( <> {errors.name && {errors.name}} {errors.avatar && {errors.avatar}} {progress && ( )} )} ) } ``` **Choosing `
` vs `useForm` for uploads:** - **File submits with other fields** (avatar + name, one Save button) → file input inside `` - **Standalone immediate upload** (uploads on select, no Save button) → `` + `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) — `` with `#default` scoped slot, `useForm` returns reactive proxy (`form.email` not `setData`), `v-model` binding - **Svelte**: [`references/svelte.md`](references/svelte.md) — `` 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 | `` 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 `` 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" | `` + `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 ``, Active Storage integration, direct upload setup, multiple files, and progress tracking. [View on SkillFed](https://skillfed.io/inertia-rails/skills/inertia-rails-forms) · [View on GitHub](https://github.com/inertia-rails/skills)