Forms
> Handle form submissions with Inertia's useForm hook for validation, error handling, and submission states.
Inertia’s useForm hook provides form state management with validation error handling and submission states.
Basic Form
// resources/js/pages/Posts/Create.tsx
import { useForm, Head, Link } from '@inertiajs/react'
import Layout from '@/layouts/Layout'
export default function CreatePost() {
const { data, setData, post, errors, processing } = useForm({
title: '',
body: '',
})
function submit(e: React.FormEvent) {
e.preventDefault()
post('/posts')
}
return (
<Layout>
<Head title="Create Post" />
<div className="container mx-auto px-4 max-w-2xl">
<h1 className="text-3xl font-bold mb-6">Create New Post</h1>
<form onSubmit={submit} className="space-y-6">
<div>
<label htmlFor="title" className="block text-sm font-medium mb-2">
Title
</label>
<input
id="title"
type="text"
value={data.title}
onChange={e => setData('title', e.target.value)}
className={`w-full px-3 py-2 border rounded-md ${
errors.title ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.title && (
<div className="text-red-500 text-sm mt-1">{errors.title}</div>
)}
</div>
<div>
<label htmlFor="body" className="block text-sm font-medium mb-2">
Content
</label>
<textarea
id="body"
value={data.body}
onChange={e => setData('body', e.target.value)}
rows={10}
className={`w-full px-3 py-2 border rounded-md ${
errors.body ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.body && (
<div className="text-red-500 text-sm mt-1">{errors.body}</div>
)}
</div>
<div className="flex justify-between">
<Link
href="/posts"
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</Link>
<button
type="submit"
disabled={processing}
className="bg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{processing ? 'Creating...' : 'Create Post'}
</button>
</div>
</form>
</div>
</Layout>
)
}useForm API
const {
data, // Current form data
setData, // Update form field
post, // Submit via POST
put, // Submit via PUT
patch, // Submit via PATCH
delete: del, // Submit via DELETE
errors, // Validation errors from server
processing, // True during submission
progress, // Upload progress (for files)
reset, // Reset form to initial values
clearErrors, // Clear validation errors
transform, // Transform data before submission
} = useForm({
title: '',
body: '',
})Updating Fields
// Single field
setData('title', 'New Title')
// Multiple fields
setData({
title: 'New Title',
body: 'New Body',
})
// Callback form (for derived values)
setData(data => ({
...data,
slug: data.title.toLowerCase().replace(/\s+/g, '-'),
}))Form Methods
// Create
post('/posts')
// Update
put(`/posts/${post.id}`)
patch(`/posts/${post.id}`)
// Delete
del(`/posts/${post.id}`)
// With options
post('/posts', {
preserveScroll: true,
preserveState: true,
onSuccess: () => {
reset()
},
onError: (errors) => {
console.log('Validation errors:', errors)
},
})File Uploads
import { useForm } from '@inertiajs/react'
export default function FileUpload() {
const { data, setData, post, progress } = useForm({
avatar: null as File | null,
})
function submit(e: React.FormEvent) {
e.preventDefault()
post('/profile/avatar', {
forceFormData: true,
})
}
return (
<form onSubmit={submit}>
<input
type="file"
onChange={e => setData('avatar', e.target.files?.[0] || null)}
accept="image/*"
/>
{progress && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${progress.percentage}%` }}
/>
</div>
)}
<button type="submit">Upload</button>
</form>
)
}Transform Data
Modify data before submission:
const { data, setData, post, transform } = useForm({
name: '',
remember: false,
})
function submit(e: React.FormEvent) {
e.preventDefault()
transform(data => ({
...data,
remember: data.remember ? 'on' : '',
}))
post('/login')
}Resetting Forms
const { data, setData, reset } = useForm({
title: '',
body: '',
})
// Reset all fields
reset()
// Reset specific fields
reset('title')
reset('title', 'body')Validation Errors
Errors come from Go validation and are keyed by field name:
// Go handler
func (c *PostHandler) Store(ctx *router.Context) error {
errors := validate.Check(ctx.Request, validate.Rules{
"title": {"required", "min:3"},
"body": {"required", "min:10"},
})
if errors.HasErrors() {
view.RenderWithErrors(ctx.Response, ctx.Request, "Posts/Create",
view.Props{}, errors)
return nil
}
// Create post...
return nil
}// React component
{errors.title && (
<span className="text-red-500">{errors.title}</span>
)}Form Callbacks
post('/posts', {
onBefore: () => {
// Called before request
return confirm('Are you sure?')
},
onStart: () => {
// Request started
},
onProgress: (progress) => {
// Upload progress update
},
onSuccess: (page) => {
// Request succeeded
reset()
},
onError: (errors) => {
// Validation errors
},
onCancel: () => {
// Request was cancelled
},
onFinish: () => {
// Always called (success or error)
},
})Preserving State
// Keep scroll position after submission
post('/posts', { preserveScroll: true })
// Keep form state on error (default for errors)
post('/posts', { preserveState: true })
// Replace history entry (no back button to form)
post('/posts', { replace: true })Best Practices
- Disable Buttons - Use
processingto disable submit during submission - Show Progress - Display upload progress for file forms
- Clear on Success - Reset form after successful submission
- Preserve Scroll - Use
preserveScrollfor inline forms - Transform Data - Use
transformfor data modifications before submit - Type Props - Define TypeScript interfaces for form data