npx claudepluginhub inertia-rails/skillsThis skill uses the workspace's default tool permissions.
Full-stack form handling for Inertia.js + Rails.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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.