With Inertia's useForm helper, how do data, setData, processing and errors work together on a customer edit form?
answer
- one stateful object per form
- setData(key, value), object or updater
- form.put(url) makes an Inertia visit
- processing: onStart to onFinish
- errors: field name to first message
basics
~20 suseForm holds the form's fields in data, updates them through setData, and submits them as an Inertia visit with put, post or patch; processing stays true while that visit runs, and errors fills with one message per field when Laravel validation fails.
solid answer
~40 s`useForm({ name: customer.name, email: customer.email })` returns one object for the whole form. In React the values live in `data` and change through `setData('email', value)`, which also takes an object or an updater function; Vue and Svelte expose the fields directly on the form. `form.put('/customers/42')` (or `post`, `patch`, `delete`, `get`, `submit(method, url)`) makes an Inertia visit with the current data and accepts normal visit options such as `preserveScroll` and `onSuccess`. `processing` becomes `true` when the request starts and, in Inertia 3, returns to `false` only in `onFinish`, so binding the button's `disabled` to it blocks double submits. When validation fails, `errors` becomes an object keyed by field holding the first message each, `hasErrors` turns true; on success `wasSuccessful` and the two-second `recentlySuccessful` flag drive a saved notice.
code
jsx · 24 linesimport { useForm } from '@inertiajs/react'
export default function Edit({ customer }) {
const { data, setData, put, processing, errors, recentlySuccessful } = useForm({
name: customer.name,
email: customer.email,
})
function submit(e) {
e.preventDefault()
put(`/customers/${customer.id}`, { preserveScroll: true })
}
return (
<form onSubmit={submit}>
<input value={data.name} onChange={(e) => setData('name', e.target.value)} />
{errors.name && <p>{errors.name}</p>}
<input value={data.email} onChange={(e) => setData('email', e.target.value)} />
{errors.email && <p>{errors.email}</p>}
<button disabled={processing}>Save</button>
{recentlySuccessful && <span>Saved.</span>}
</form>
)
}go deeper
Recall the shape: useForm with initial values, setData to change a field, put or post to submit, errors to show messages, processing to disable the button.
Explain the lifecycle behind processing and progress, why errors arrive keyed by field with one message each, and how setData's object and updater forms differ.
Show you know a form submission is a visit, not a request you await: outcomes arrive through callbacks and props, and the Inertia 3 onFinish timing affects double-submit guards.
Weigh the helper's built-in state against a separate client-side form library, considering duplicated validation logic, consistency across teams and the cost of two state models.
## What useForm is **Inertia** lets a Laravel controller return a JavaScript page component (React, Vue or Svelte) instead of a Blade view, and turns link clicks and form submissions into XHR **visits** that swap the page without a full reload. The **`useForm`** helper, imported from your adapter (`@inertiajs/react`, `@inertiajs/vue3`, `@inertiajs/svelte`), is the stateful object you build a form around. You pass it the initial values, and it gives back everything a form needs: - the current field values; - a way to change them; - submit methods that send an Inertia visit; - request state (`processing`, `progress`); - validation state (`errors`, `hasErrors`); - success flags (`wasSuccessful`, `recentlySuccessful`). For a customer edit page the initial values usually come from the controller's props: `useForm({ name: customer.name, email: customer.email, phone: customer.phone })`. ## Reading and writing fields In React the values sit in `form.data` and are immutable React state, so you change them through **`setData`**. Vue and Svelte expose each field directly (`form.email`) and you bind inputs with `v-model` or `bind:value`. | React call | Effect | |---|---| | `setData('email', value)` | sets one field (dotted keys such as `'address.city'` reach nested values) | | `setData({ name, email, phone })` | replaces the whole data object | | `setData(prev => ({ ...prev, phone: '' }))` | computes the next data from the previous data | Because the object owns the values, you do not keep a parallel `useState` per input, and the helper can compare them with the initial values for `isDirty`. ## Submitting The form object has **`get`, `post`, `put`, `patch` and `delete`**, each taking a URL and optional visit options, plus `submit(method, url, options)`. `form.put('/customers/42', { preserveScroll: true })` sends a PUT visit whose body is the current data (after any `transform()` callback). It is still an Inertia visit, not a `fetch` you await: the Laravel controller answers with a redirect or an Inertia page, and the client swaps props. Callbacks such as `onSuccess`, `onError` and `onFinish` are how you react to the outcome. ## The request lifecycle and processing The helper wraps the visit's events and updates its own state: 1. `onBefore` clears `wasSuccessful` and `recentlySuccessful`. 2. `onStart` sets **`processing`** to `true`. 3. `onProgress` fills **`progress`** while a file upload is in flight (it stays `null` otherwise). 4. `onSuccess` or `onError` updates the success flags or the errors. 5. `onFinish` sets `processing` back to `false` and `progress` back to `null`. Since **Inertia 3**, that reset happens only in `onFinish`; earlier releases cleared `processing` as soon as a response arrived. The practical rule is unchanged: bind the submit button's `disabled` attribute to `processing` and a second click cannot fire a second PUT while the first visit is still being handled. ## errors, hasErrors and success flags When Laravel validation fails, the Inertia adapter delivers the messages as page props, and the helper copies them into **`errors`**: a plain object keyed by field name, with the **first message** per field as a string (the server middleware can switch that to arrays). Related members: - **`hasErrors`**: `true` when `errors` has any key; - **`clearErrors()`** / `clearErrors('email')`: removes all or some messages, for example when the user edits the field; - **`setError('email', 'Already taken')`**: sets messages yourself, for client-side checks; page props stay untouched; - **`wasSuccessful`**: `true` after the last submission succeeded; - **`recentlySuccessful`**: `true` for 2000 ms after success (configurable as `form.recentlySuccessfulDuration` in the app defaults), made for a brief "Saved" label. These errors are scoped to the form object that submitted, so a second `useForm` on the same page does not show them. ## Putting it together ```jsx const form = useForm({ name: customer.name, email: customer.email }) function submit(e) { e.preventDefault() form.put(`/customers/${customer.id}`, { preserveScroll: true }) } ``` The input for `email` reads `form.data.email`, writes with `form.setData('email', e.target.value)`, shows `form.errors.email` when present, and the button uses `disabled={form.processing}`. After a failed save, the typed values remain because the component stays mounted; after a successful save, `recentlySuccessful` shows the confirmation and the helper adopts the saved values as its new defaults.
- Besides a key and a value, what does React's setData accept?A whole object, which replaces the form data, or an updater function that receives the previous data and returns the next object. The updater form is the safe choice when the new value depends on the old one, for example clearing `phone` when a `no_phone` checkbox is ticked, because it never reads a stale render's data.
- How do useForm's errors differ from page.props.errors on the same page?`page.props.errors` is the global prop the Laravel adapter shares on every response. The helper copies the errors of its own visit into `form.errors`, so a second form on the page stays clean. `setError` only changes the form object, never the page props, which makes it the tool for client-side checks.
- Why bind the button to processing instead of a local useState flag?`processing` follows the visit's real lifecycle: it turns on in `onStart` and off in `onFinish`, which also fires after a cancel or a network error. A hand-rolled flag has to be reset on every one of those paths, and a missed path leaves the button disabled forever or lets a double submit through.
saying these in an interview costs you the question
- useForm is a fetch wrapper whose promise resolves with the controller's JSON
- processing must be switched on and off by hand in the callbacks
- errors holds every message for each field as an array by default
- Calling setError also rewrites page.props.errors
- In Inertia 3, processing drops to false the moment response headers arrive