skip to content

How does Inertia 3's built-in Precognition give a customer form live validation with validate() and invalid(), and what must the Laravel route provide?

level: seniorimportance: nice to knowfreq 22%

answer

  1. same URL, Precognition: true header
  2. precognitive middleware alias
  3. rules must live in a form request
  4. 204 with Precognition-Success, or 422
  5. debounced 1500 ms, files skipped

basics

~20 s

Precognition sends the form to its real route with a Precognition header; Laravel's precognitive middleware runs validation from the route's form request but not the controller, answering 204 or 422, and useForm's validate() and invalid() show the result per field.

solid answer

~40 s

In Inertia 3, Precognition is built into the form helper: `useForm('put', '/customers/42', data)` or `useForm(data).withPrecognition('put', url)`, and `<Form>` is always capable. `validate('email')` sends the current data to the same URL with `Precognition: true` and `Precognition-Validate-Only: email`, debounced at 1500 ms, skipping files unless `validateFiles()`. The route needs Laravel's `HandlePrecognitiveRequests` middleware (alias `precognitive`) and its rules in a **form request**: during a precognitive request Laravel runs middleware and resolves controller dependencies, which validates the form request limited to the listed fields, but never runs the controller method. Clean data answers 204 with `Precognition-Success: true`; failures come back as 422 JSON because the request accepts JSON. `invalid('email')` is true when the form's errors hold that field, `valid('email')` once it validated cleanly, and `validating` shows a request in flight.

code

php · 11 lines
php
<?php

use App\Http\Controllers\CustomerController;
use Illuminate\Support\Facades\Route;

Route::put('/customers/{customer}', [CustomerController::class, 'update'])
    ->middleware('precognitive');

// CustomerController::update(UpdateCustomerRequest $request, Customer $customer)
// The form request validates while parameters resolve;
// on a precognitive request the method body never runs.

go deeper

for a junior

Recall that validate('field') asks the server whether a field is valid and invalid('field') shows whether it has an error.

for a middle

Explain the Precognition header, the precognitive middleware, the 204 versus 422 responses, and why rules must sit in a form request.

for a senior

Harden it: skip middleware side effects with isPrecognitive(), relax costly rules during precognition, tune the debounce, and decide when files need validateFiles().

for a principal

Judge whether live server validation is worth the extra requests for a given form, considering server load, perceived speed and keeping one source of rules.

## What Precognition is **Laravel Precognition** is a protocol for asking the server "would this request pass validation?" without running the action. The frontend sends the real request, to the real URL, with a `Precognition: true` header; the backend runs everything up to the controller method and stops. The result is live validation that uses the exact server-side rules, with nothing duplicated in JavaScript. Since **Inertia 2.3**, and in Inertia 3, the client side is built into the adapters' form APIs, so the separate `laravel-precognition` frontend packages are no longer needed in an Inertia app (the adapters depend on Precognition's core library internally). ## Enabling it on the client With `useForm`, give the form a method and URL for validation: - `useForm('put', '/customers/42', { name, email })`, the form accepted for compatibility with the old packages; - `useForm({ name, email }).withPrecognition('put', url)`; - a Wayfinder route object in place of method and URL. The `<Form>` component always has Precognition wired to its own `action` and `method`, so its slot's `validate` works with no extra setup. The members you use: | Member | Meaning | |---|---| | `validate('email')` | validate one field now (debounced) | | `validate({ only: ['name', 'email'] })` | validate several, for wizard steps | | `touch('email')` then `validate()` | validate every touched field | | `invalid('email')` | `true` when the form's errors contain the field | | `valid('email')` | `true` once the field was validated and has no error | | `validating` | a validation request is in flight | ## What the request looks like 1. The helper sends the current data with the form's method to the form's URL. 2. Headers include `Precognition: true`, `Precognition-Validate-Only: email` and `Accept: application/json`. 3. Requests are **debounced**: the first fires at once and later ones wait 1500 ms by default (`setValidationTimeout()` or the `validationTimeout` prop change it). 4. A field is only sent once its value differs from the initial data. 5. **Files are excluded** unless you call `validateFiles()` (`validateFiles` prop on `<Form>`). 6. Errors are reduced to the first message per field unless `withAllErrors()` is called. ## What the Laravel route must provide - The **`HandlePrecognitiveRequests`** middleware, registered as the `precognitive` alias in Laravel 13: `Route::put('/customers/{customer}', ...)->middleware('precognitive')`. - Validation rules in a **form request** type-hinted on the action. During a precognitive request the middleware swaps in dispatchers that resolve the action's parameters, which is when a form request validates, and then abort with **204** and `Precognition-Success: true` without calling the method. - The form request filters its rules down to the fields named in `Precognition-Validate-Only`, so untouched fields do not report errors early. Rules written inline with `$request->validate()` inside the controller method never run during a precognitive request, because the method body never runs; every such request would come back as a success. ## Common mistakes - adding the client calls but forgetting `->middleware('precognitive')`, so the "validation" request runs the real update; - keeping rules inline in the controller method, where a precognitive request never reaches them; - expecting an avatar field to be checked live without `validateFiles()`; - calling `validate('email')` before the field changed and wondering why no request is sent. ## Responses and side effects | Outcome | Response | |---|---| | all requested fields pass | `204 No Content`, `Precognition-Success: true` | | a rule fails | `422` JSON errors, since the request accepts JSON | This is the one place an Inertia app sees a 422, and the helper handles it without navigating. Responses also carry `Precognition: true` and `Vary: Precognition`. Because **all route middleware still runs**, middleware with side effects such as counting interactions should check `$request->isPrecognitive()` and skip. Form requests can use the same check to relax expensive rules, for example running an uncompromised-password rule only on the real submission.

  • Why does validation with $request->validate() inside the controller never fail precognitively?
    A precognitive request stops after resolving the action's parameters and answers 204; the method body, where the inline `validate()` call lives, never executes. Only a form request, which validates while it is resolved, is checked.
  • How do you stop a password field from being checked against a breach list on every keystroke?
    In the form request's `rules()`, branch on `$this->isPrecognitive()`: send only cheap rules such as required and a minimum length during precognition, and add the expensive rule for the real submission.

saying these in an interview costs you the question

  • Precognition runs the controller in a rolled-back transaction
  • Inertia 3 still needs the laravel-precognition npm package
  • Rules written inline with $request->validate() are checked precognitively
  • Every keystroke sends a request with no debounce
  • Uploaded files are validated precognitively by default