--- 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 `
` 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?** → `` 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.