Inertia Forms and Validation

+15 Mana ✨

Introduction

Inertia bridges Laravel's server-side validation with your Vue or React form without forcing you to build a REST API. The useForm helper gives you a reactive form object that knows how to POST to your controllers, surface validation errors, and track loading state — all while letting Laravel's validate() method do the work it's good at.

Key Concepts

  • useForm: A reactive form helper imported from @inertiajs/vue3 (or @inertiajs/react) that wraps field state, submission, validation errors, and processing flags.
  • Server-side validation: Laravel's $request->validate([...]) method. When validation fails, Inertia's request adapter converts the redirect-with-errors response into populated form.errors fields client-side.
  • form.processing: A reactive boolean that is true while the request is in flight. Disable the submit button with it to prevent double submissions.

Real World Context

A broken signup form costs conversions. With Inertia, you get instant inline error messages for every field (validated server-side so the rules stay authoritative) and a guaranteed-correct disabled state while the request is pending — without shipping a JSON API.

Deep Dive

The Controller

The controller looks exactly like a classic Laravel form handler:

php
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'email', 'unique:users,email'],
        'password' => ['required', 'min:8', 'confirmed'],
    ]);

    User::create($validated);

    return redirect()->route('users.index')
        ->with('success', 'User created');
}

No API resource, no JSON response — Inertia intercepts the validation redirect and surfaces the errors on the frontend automatically.

The Vue Form

vue
<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
    name: '',
    email: '',
    password: '',
    password_confirmation: '',
})

const submit = () => {
    form.post('/users', {
        onSuccess: () => form.reset('password', 'password_confirmation'),
    })
}
</script>

<template>
    <form @submit.prevent="submit">
        <label>
            Name
            <input v-model="form.name" type="text">
            <span v-if="form.errors.name" class="text-red-600">{{ form.errors.name }}</span>
        </label>

        <label>
            Email
            <input v-model="form.email" type="email">
            <span v-if="form.errors.email" class="text-red-600">{{ form.errors.email }}</span>
        </label>

        <label>
            Password
            <input v-model="form.password" type="password">
            <span v-if="form.errors.password" class="text-red-600">{{ form.errors.password }}</span>
        </label>

        <label>
            Confirm Password
            <input v-model="form.password_confirmation" type="password">
        </label>

        <button type="submit" :disabled="form.processing">
            {{ form.processing ? 'Creating…' : 'Create user' }}
        </button>
    </form>
</template>

Three things to notice:

  • form.errors.name is populated automatically from Laravel's validation response.
  • form.processing is a reactive flag flipped during the request.
  • form.reset('password', 'password_confirmation') clears sensitive fields after a successful submit.

Partial Reloads and Preservation

Inertia's request options let you preserve scroll position, keep specific form fields, and reload only specific props:

javascript
form.post('/users', {
    preserveScroll: true,
    preserveState: (page) => Object.keys(page.props.errors).length > 0,
})

Here we preserve scroll on any response and preserve component state (so the form doesn't reset) only when the response includes validation errors.

Common Pitfalls

  1. Hand-rolling axios.post and manually parsing errors — You lose form.processing, reset helpers, and the built-in error mapping. Use useForm instead.
  2. Resetting the whole form on success — Calling form.reset() with no arguments clears every field. Pass specific field names for password flows.
  3. Double submissions — Forgetting :disabled="form.processing" lets an impatient user click twice and create duplicate records.

Best Practices

  1. Trust the server — Let Laravel's validate() method be the only source of truth for validation rules. Client-side echoes are fine, but the server must always validate.
  2. Use onSuccess / onError callbacks — They give you a single place to reset fields, show toasts, or redirect after the response.
  3. Name your form fields to match Laravel's — Inertia maps form.errors.X to validation errors keyed by X. Keep them aligned.

Summary

  • useForm wraps field state, submission, errors, and loading in a single reactive object.
  • Laravel's $request->validate() remains the source of truth — Inertia surfaces errors client-side automatically.
  • form.processing disables the submit button during the request, preventing double submissions.
  • Use preserveScroll / preserveState options to fine-tune the user experience.
✓ Completed