livewire/livewire: Preserve non-HTML attributes for Blade props in v4
Contributed by ghabriel25
Part of the livewire v4.4.7 release
Among the changes you'll meet when moving from Livewire v3.8.10 to v4 is a tightened approach to HTML attribute forwarding. One recent v4 fix (#10649) stopped arrays and objects from being rendered as HTML attributes on your components — a good change — but it inadvertently broke a common pattern: reading those values back in the child component through Blade's @props. Pull request #10682 resolves that regression (it fixes issue #10680). Non-HTML values passed to a <livewire:...> component are once again available to the component's view for use with @props, while remaining firmly excluded from your rendered markup and the component snapshot. If you pass structured data — arrays, objects, or Eloquent models — into child components, this fix directly affects your upgrade.
What changed
Livewire's attribute forwarding feature handles every extra parameter you pass to a component tag that isn't declared as a public property. In v4, only values that make sense as HTML — scalars, null, and Htmlable instances — are collected into the $attributes bag forwarded to the child view. Arrays and objects are deliberately excluded so they can't leak into markup or be dehydrated into the component snapshot.
The regression was that those filtered-out values were dropped entirely: they never reached the view, so @props silently fell back to its defaults. This PR adds a getNonHtmlAttributes() method that computes the complement — everything passed in that can't be rendered as HTML — and injects those values into the view via $view->with(...). A shared forwardAttributesToView() helper now applies the same treatment to regular renders, islands, and lazy-loading placeholders.
Why it matters
Consider a typical list component view:
@props(['rows' => []])
@foreach ($rows as $row)
...
@endforeach
Between the earlier v4 fix and this PR, a parent passing :rows="$rows" would get the default empty array. The component rendered nothing even though the data was right there — a quiet failure that's easy to miss until runtime. Blade's @props exists precisely for this hand-off pattern, so restoring it keeps the component API predictable.
The fix in practice
From the parent view:
<livewire:alert :rows="$rows" id="rows-component" />
And the child component's view:
@props(['rows' => []])
<ul {{ $attributes }}>
@foreach ($rows as $row)
<li wire:key="{{ $row['id'] }}">{{ $row['name'] }}</li>
@endforeach
</ul>
Here's what you get: $rows resolves through @props, so the list renders its items. The scalar id is forwarded as a real HTML attribute via {{ $attributes }}, producing <ul id="rows-component">. Nothing resembling rows="..." appears in the markup. Using @props without a default (@props(['rows'])) works as well.
What still doesn't leak
Two guarantees carry over from the earlier fix. First, non-HTML values are never rendered into attributes — the new tests assert the string rows= never appears in the output. Second, they aren't dehydrated into the component snapshot: the suite mounts an Eloquent model as a record prop and asserts the model's email never shows up in the rendered HTML. These values are view-only handoffs at render time, not reactive state — if you need two-way binding or server-side tracking, declare a public property instead. Lazy components are covered too: the placeholder view receives the same non-HTML values, so @props keeps working during placeholder rendering.
Upgrade impact
Coming from v3.8.10, here's what to check:
- If you relied on arrays or objects showing up in the forwarded
$attributesbag, that behavior is gone in v4 — and it was never reliable. Switch to@propsas shown above. - If you adopted early v4 builds, hit this regression, and worked around it — declaring public properties just to shuttle data into the view, or merging values in manually — you can remove those workarounds now.
id,class, and other scalar attributes continue to forward exactly as before; there's no change there.- Anything passed through attribute forwarding is render-time-only, not persistent component state.
No action is needed if your components already use @props with scalar defaults — this PR simply makes the structured-data case work again.
Takeaways
- Arrays, objects, and models passed to components are once again available to
@propsin v4. - They are never rendered as HTML attributes and never dehydrated into the snapshot.
- Scalar attributes like
idandclassforward via{{ $attributes }}unchanged. - Works for standard renders, islands, and lazy placeholders alike.
- Remove any workarounds you added after the earlier v4 behavior change.