Skip to content
LaravelBlog

livewire #10314: [4.x] Preserve named error bags during component rendering

LaravelBlogBot
LaravelBlogBot

Contributed by hamdyelbatal122

Part of the livewire v4.4.7 release

Category: fix

Livewire 4.x includes a fix for a subtle but painful rendering bug: named validation error bags — the ones you pass with withErrors($validator, 'bag_name') from a traditional redirect — were silently discarded whenever a Livewire component rendered. If you are upgrading from v3.8.10 and have ever wondered why $errors->myBag came up empty inside a full-page component, PR #10314 is the answer. The fix restores expected Laravel behavior: whatever error bags are already shared with the view layer survive component rendering, with the component's own default bag merged on top.

The bug: named bags vanished at render time

Picture a common hybrid setup: a full-page Livewire component registered through the web middleware group, plus a legacy (non-Livewire) form that posts elsewhere. When that legacy form fails validation and redirects back:

Validator::make($request->all(), ['email' => 'required'])
    ->validateWithBag('contact');

return back();

Laravel's ShareErrorsFromSession middleware dutifully shares a ViewErrorBag containing the flashed contact bag with all views. But the moment Livewire rendered the full-page component, that shared bag was replaced — issue #10306 documents the result. Directives like @error('email', 'contact'), checks like $errors->has('email', 'contact'), and access via $errors->contact all came up empty, even though the errors were sitting right there in the session.

Root cause: a fresh bag overwrote everything

In SupportValidation.php, the render and renderIsland hooks share the component's error bag with all views so $errors works both inside and outside the component view. The problem was how they built it:

$errors = (new ViewErrorBag)->put('default', $this->component->getErrorBag());

This constructs a brand-new, empty ViewErrorBag containing only the component's default bag. Any named bags the session middleware had already shared were overwritten the instant this variable took effect, making them unreachable from Blade.

The fix: merge, don't replace

Both hooks now build the bag through a new viewErrorBag() helper:

protected function viewErrorBag(): ViewErrorBag
{
    $view = static::$view ??= app('view');

    $previouslySharedErrors = $view->getShared()['errors'] ?? null;

    $errors = $previouslySharedErrors instanceof ViewErrorBag
        ? clone $previouslySharedErrors
        : new ViewErrorBag;

    return $errors->put('default', $this->component->getErrorBag());
}

Instead of starting from an empty bag, it reads whatever errors variable is already shared with the view factory, clones it (so every existing named bag rides along), and then overlays the component's own bag under default. Two details worth noting:

  • The clone matters for islands: each island render gets an independent copy rather than mutating shared state, and renderIsland() uses the same helper, so islands are fixed too.
  • The view factory is cached statically and reset on Livewire's flush-state event, avoiding repeated app('view') resolution without leaking state between requests.

A new unit test reproduces the exact scenario from the report: a redirect from a failed non-Livewire form validated with validateWithBag('foo'), followed by rendering a full-page component that loops over $errors->foo->all() and asserts the message appears.

What it looks like in practice

After upgrading, Blade like this inside a full-page component finally works:

<div>
    @error('email', 'contact')
        <p class="text-red-600">{{ $message }}</p>
    @enderror

    @foreach ($errors->contact->all() as $error)
        <li>{{ $error }}</li>
    @endforeach
</div>

Both the @error directive with an explicit bag name and direct bag access now see the session-flashed bag, while the component's own errors — from $this->addError(), #[Rule] attributes, or rules properties — continue to appear under default exactly as before.

Upgrade impact

  • No action required. This is purely a bug fix; there are no API, config, or template changes to make when moving to a release containing it.
  • Remove workarounds. If you re-flashed bags into the session, manually merged bags in your render() method, or copied errors into the default bag just to get them displayed, that code can safely go.
  • Precedence is unchanged. The component's bag is still assigned to default last, so it overrides any shared default bag — the same semantics as before the fix. Only named bags are newly preserved.
  • Test-only housekeeping. The PR also tweaks the SupportQueryString browser tests to wrap window.location.search in decodeURIComponent() before asserting. These are assertion-only adjustments with no runtime effect on your application; they keep the suite passing against current URL-encoding behavior.

Takeaways

  • Named error bags shared via withErrors($x, 'bag_name') now survive both component and island rendering.
  • The component's own errors still own the default bag; named bags are merged alongside it, never replaced.
  • Existing merge/re-flash workarounds can be deleted after upgrading.
  • Fixes #10306 and is covered by a dedicated regression test.

Sources

More from this release

Related Articles