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
