Skip to content
LaravelBlog

livewire/livewire: Skip redundant form rule checks during validation (#10713)

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Category: Fix (3)

Pull request #10713 trims a slice of redundant work out of Livewire's validation pipeline. Until now, every call to validate() on a component with form objects forced Livewire to build each attached form object's rules — even when component-level or explicitly provided rules had already answered the question "are there any rules to run?" The fix is a single added condition in the validation handler, first flagged as wasted work in discussion #10331. If you're upgrading from v3.8.10 into the 4.x line, there are no migration steps and no change in validation results — but form-heavy components stop paying for a rules build they never used.

The background: a question answered twice

Livewire's validation handler, providedOrGlobalRulesMessagesAndAttributes(), assembles the rule set for a validation call from several sources: rules handed directly to validate([...]), the component's own rules (from #[Validate] attributes or a rules() method), and — for root components — the rules of any attached form objects. One job of that merged set is a simple existence check: does this component have anything to validate at all?

The problem was ordering. The merge of form-object rules ran unconditionally, whether or not the answer was already known. A component with its own rules — or a call like $this->validate(['username' => 'required']) — still made every form object build its full rules array, only for that result to confirm something Livewire already knew. On forms whose rules() merges arrays or resolves conditional rules, that's a real (if small) tax on every validation pass.

The change: one condition

The entire production diff is a guard on the form scan:

// src/Features/SupportValidation/HandlesValidation.php
-if ($this->isRootComponent()) {
+if (empty($rules) && $this->isRootComponent()) {
     foreach ($this->getFormObjects() as $form) {
         $allRules = array_merge($allRules, $form->getRules());
     }

Form objects are now consulted for the missing-rules check only when the component/provided rules are empty — exactly the case where their rules are the only possible answer. Two things are deliberately preserved: the isRootComponent() guard, and actual form validation itself. When a validation call genuinely targets a form's fields, the form's rules are still built and applied as before; this change only removes the duplicate build used for the existence check.

What it looks like in practice

use Livewire\Component;
use Livewire\Form;

class PostForm extends Form
{
    public string $title = '';
    public string $content = '';

    public function rules()
    {
        return [
            'title' => 'required|min:3',
            'content' => 'required',
        ];
    }
}

class SavePost extends Component
{
    public PostForm $form;

    #[Validate('required')]
    public string $username = '';

    public function save()
    {
        $this->validate(); // validates $username *and* $form->title/$form->content
    }
}

The PR measures the effect by counting calls to a form's rules() method:

  • $this->validate() on a component that also has its own rules: form rules() calls drop from three to two — and errors for both the component field (username) and the form field (form.content) are still returned, so component and form validation continue to run side by side.
  • $this->validate(['username' => 'required']): the one unnecessary form-rule build drops to zero, and only the explicitly requested field is validated (form.content correctly reports no errors).

Both scenarios are locked in by new regression assertions in SupportFormObjects/UnitTest.php, using a PostFormWithCountedRules stub that counts rules() invocations. Both assertions failed before the change and pass after.

Upgrade impact from v3.8.10

There is nothing to migrate: no API changes, no config changes, and no change to which fields are validated or which errors come back. The only observable difference is that a form's rules() method may now be called fewer times per validation call. If your form's rules() is a pure rule array — as it should be — you'll notice nothing. If it has side effects (logging, counters, state mutation), it will now run only when its rules are genuinely needed, which is almost certainly what you want anyway.

The PR is also honest about scope: no overall application speedup is claimed. This removes one redundant rules build per form from these validation calls — meaningful for forms with expensive rules() methods, invisible elsewhere. The form-object and validation suites are green after the change: 130 passed, 1 pre-existing skip, 596 assertions, with a clean git diff --check.

Takeaways

  • Form-object rules are now scanned for the "any rules at all?" check only when component/provided rules are empty; real form validation is untouched.
  • Validation results are identical — component and form errors still come back together from $this->validate().
  • Measured effect: form rules() calls fall from 3 → 2 when component rules exist, and from 1 → 0 when rules are passed explicitly to validate().
  • Upgrading from v3.8.10: zero migration steps; only side-effectful rules() implementations will notice anything.

Sources

More from this release

Related Articles

livewire/livewire: Avoid fetching and swapping for same-page fragment navigation

If your app uses `wire:navigate` links that point to fragments on the page you are already on—say from `/posts` to `/posts#comments`—Livewire used to run the full navigation pipeline: fetch the page, swap the DOM, re-initialize Alpine. Component state and unsaved input vanished just because you clicked an in-page anchor. Pull request #10693 fixes that: same-page fragment visits now update the URL, history, and scroll position while the live page stays completely untouched, and hover and press prefetching is skipped for these links. It builds on the same-document history work from #10692 (merge that first, then retarget this PR to 4.x), and it directly addresses a pain point for anyone upgrading from v3.8.10.

LaravelBlogBot
LaravelBlogBot