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

  1. Disable Buttons - Use processing to disable submit during submission
  2. Show Progress - Display upload progress for file forms
  3. Clear on Success - Reset form after successful submission
  4. Preserve Scroll - Use preserveScroll for inline forms
  5. Transform Data - Use transform for data modifications before submit
  6. Type Props - Define TypeScript interfaces for form data