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. On the server,
validate the request, then flash the errors and old input and redirect back.
ctx.FlashErrors and ctx.FlashInput write short-lived encrypted flash
cookies; the view layer reads them on the next render and injects them as
the errors and old props:
// Go handler
import (
"github.com/velocitykode/velocity/router"
"github.com/velocitykode/velocity/validation"
"github.com/velocitykode/velocity/view"
)
func (c *PostHandler) Store(ctx *router.Context) error {
// CheckW threads the ResponseWriter through so an oversized body can
// signal the connection to close. The error return is a malformed
// rule set (a handler bug), never a field-level failure.
result, err := validation.CheckW(ctx.Response, ctx.Request, validation.Rules{
"title": {validation.Required(), validation.Min(3)},
"body": {validation.Required(), validation.Min(10)},
})
if err != nil {
return err
}
if result.HasErrors() {
ctx.FlashErrors(result.All())
ctx.FlashInput(result.Old())
view.Back(ctx)
return nil
}
// Create post...
return nil
}Rules are typed constructor values collected in a validation.Rules set
keyed by field, not strings. result.All() is a map[string]string (one
message per field), which is the shape Inertia’s errors prop expects.
result.Old() strips sensitive-looking fields (anything whose name
contains password, token, secret, card, otp, and similar) before
handing input back for replay, so a password never round-trips to the
client.
validation.Unique and validation.Exists execute in the
validation/dbrules subpackage, which owns the orm dependency. Use
dbrules.CheckWithDBW(ctx.Response, ctx.Request, rules, db) when your rule
set names them, or let ctx.Validate / ctx.BindValid / vform.Form do
it, since those resolve the database from the request’s service container
for you.vform.Form
For the common case, vform.Form[T] binds the request body into a typed
struct, validates it, and on failure flashes errors plus old input, redirects
back, and returns router.ErrValidationAborted so the handler can return
early without the router emitting an error response:
import (
"github.com/velocitykode/velocity/router"
"github.com/velocitykode/velocity/validation"
"github.com/velocitykode/velocity/validation/vform"
)
type StorePost struct {
Title string `json:"title"`
Body string `json:"body"`
}
func (p *StorePost) Rules() validation.Rules {
return validation.Rules{
"title": {validation.Required(), validation.Min(3)},
"body": {validation.Required(), validation.Min(10)},
}
}
func (c *PostHandler) Store(ctx *router.Context) error {
post, err := vform.Form[StorePost](ctx)
if err != nil {
return err // ErrValidationAborted is handled by the router
}
// Create post using post.Title / post.Body...
return nil
}vform.FormRequest is an alias for router.Validatable, so the same
StorePost also works with ctx.BindValid(&input) when a handler needs
validation without the flash-and-redirect-back flow (a JSON endpoint, for
instance). One declaration, both entry points.
// 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