skip to content

When would you use Inertia's <Form> component instead of the useForm helper, and how does <Form> gather and submit data?

level: middleimportance: should knowfreq 30%

answer

  1. inputs identified by name attributes
  2. reads the DOM through FormData
  3. method defaults to get
  4. slot props: errors, processing, isDirty
  5. useForm for programmatic control

basics

~20 s

Inertia's <Form> suits plain forms: it reads named inputs from the DOM, submits them as a visit, and exposes errors, processing and isDirty through slot props; useForm suits forms whose data you must compute, control or remember in code.

solid answer

~40 s

`<Form action="/customers/42" method="put">` renders a real `<form>`, and on submit reads every named input through the browser's `FormData` (including the clicked submitter), converts it to an object with nested names such as `address[city]` or `address.city` expanded, and sends an Inertia visit through the form helper it wraps. You write uncontrolled inputs (in React, `defaultValue`), and a render function or slot receives `errors`, `processing`, `progress`, `isDirty`, `wasSuccessful`, `reset`, `submit` and more. Props add `transform`, `errorBag`, `disableWhileProcessing` (sets `inert`), `resetOnSuccess`, `resetOnError`, `setDefaultsOnSuccess` and visit `options`. Note that `method` defaults to `get`. Choose `useForm` when data does not live in named inputs, such as custom widgets or computed fields, or when you need `setData`, a remember key or programmatic defaults.

code

jsx · 17 lines
jsx
import { Form } from '@inertiajs/react'

export default function Edit({ customer }) {
  return (
    <Form action={`/customers/${customer.id}`} method="put" disableWhileProcessing options={{ preserveScroll: true }}>
      {({ errors, processing, isDirty }) => (
        <>
          <input name="name" defaultValue={customer.name} />
          {errors.name && <p>{errors.name}</p>}
          <input name="address.city" defaultValue={customer.address.city} />
          {errors['address.city'] && <p>{errors['address.city']}</p>}
          <button disabled={!isDirty || processing}>Save</button>
        </>
      )}
    </Form>
  )
}

go deeper

for a junior

Recall that <Form> takes action and method, reads inputs by their name attribute, and hands errors and processing to its children.

for a middle

Explain the FormData read, nested name expansion, the get default for method, the slot members and which props reset or re-baseline the form.

for a senior

Pick the right tool per screen and spot the traps: a missing method sending GET, isDirty staying true after save, and custom widgets invisible to FormData.

for a principal

Set a codebase convention for forms, balancing boilerplate against control, and decide how custom inputs expose native names so the simpler component stays usable.

## Two ways to build an Inertia form **Inertia** ships two form APIs in each adapter (React, Vue 3, Svelte 5): - the **`<Form>` component**, which behaves like an HTML form but submits through Inertia; - the **`useForm` helper**, a stateful object you bind inputs to by hand. They share one engine: `<Form>` creates a `useForm` instance internally and drives it from the DOM. So submission, errors, progress and success flags behave the same; the difference is **where the data lives**. ## How <Form> collects data `<Form>` renders a native `<form>` element and intercepts its submit event. On submit it: 1. builds a browser **`FormData`** from the form element, including the submitter button's `name` and `value`; 2. converts it to a plain object, expanding bracket names (`contacts[0][email]`) and dotted names (`address.city`) into nested objects; a backslash escapes a literal dot; 3. applies your `transform` callback, if any; 4. submits with the given `method` and `action` through the internal form helper, so files trigger a multipart body automatically. Because the DOM holds the values, inputs are **uncontrolled**: you give each one a `name`, and in React a `defaultValue` for the current record. There is no `setData`. One trap: the `method` prop defaults to **`get`**, so `<Form action="/customers/42">` without `method="put"` sends the fields as a query string to the GET route. ## What the slot exposes In React the child is a render function; Vue and Svelte pass the same object as slot props. It receives: | Group | Members | |---|---| | request state | `processing`, `progress`, `wasSuccessful`, `recentlySuccessful` | | validation | `errors`, `hasErrors`, `setError`, `clearErrors`, `resetAndClearErrors` | | change tracking | `isDirty`, `defaults()`, `reset()` | | control | `submit()`, `cancel()` | | Precognition | `validate()`, `valid()`, `invalid()`, `validating`, `touch()`, `touched()` | `isDirty` compares the current DOM values with the values captured when the form mounted. `defaults()` takes no arguments and re-captures the current values. Deep children can reach the same object through the `useFormContext()` hook, and a ref exposes it for programmatic calls. ## Props that shape the submission - `transform`: adjust data before sending, such as adding a hidden computed field; - `errorBag`: scope server errors under a named key; - `disableWhileProcessing`: adds the `inert` attribute while the visit runs; - `resetOnSuccess` / `resetOnError`: `true` or a list of field names; - `setDefaultsOnSuccess`: adopt current values as the new baseline after success, off by default; - `options`: visit behaviour after submission, such as `preserveScroll`, `only` or `replace`; - event props: `onSuccess`, `onError`, `onFinish` and the rest of the visit events. ## Common mistakes - leaving out `method="put"` on an edit form and sending a GET; - giving React inputs `value` without `onChange` instead of `defaultValue`, which makes them read-only; - reading `errors['address[city]']` when the server keys nested errors with dots, as `errors['address.city']`; - expecting `isDirty` to drop after a save without `setDefaultsOnSuccess`. ## When useForm is the better tool | Situation | Better fit | |---|---| | plain inputs mapping to request fields | `<Form>` | | a date picker or rich editor without a native input | `useForm` | | values derived from other values as the user types | `useForm` | | saving half-finished input in history with a remember key | `useForm` | | setting specific default values from code | `useForm` | | a very large form where re-rendering per keystroke hurts | `<Form>` | On a customer edit page with name, email and phone inputs, `<Form>` removes most boilerplate. Once the page grows a tag picker or an address autocomplete that is not a real input, `useForm` gives you the explicit state you need.

  • How does <Form>'s isDirty differ from useForm's after a successful save?
    `useForm` adopts submitted data as its new defaults on success, so it reads clean. `<Form>` keeps the values captured at mount unless you set `setDefaultsOnSuccess` or call `defaults()`, so a form that stays on the page after saving can still report dirty.
  • How would a nested child component show the form's errors without prop drilling?
    Call `useFormContext()` inside the child. It returns the parent `<Form>`'s exposed object, or nothing outside a form, giving access to `errors`, `isDirty`, `submit()` and `reset()`.

saying these in an interview costs you the question

  • <Form> needs every input bound to state with setData
  • <Form> sends a POST when no method prop is given
  • <Form> uses a separate submission engine from useForm
  • Nested fields require building the object in transform by hand
  • <Form> cannot send files without forceFormData