Fix Child Components That Won't Update with Livewire 4 #[Reactive] Props

Livewire reactive props aren't on by default, so a child shows a stale value. Add the #[Reactive] attribute to fix it, and know when events fit better.

Steven Richardson
Steven Richardson
· 6 min read

You wire up a filter in a parent component, pass the selected value into a child, and click around. The parent updates. The child sits there showing its first value like nothing happened. This is the single most common "why isn't Livewire working" moment, and it's not a bug — Livewire props aren't reactive by default. The fix is one attribute, but knowing when to reach for it (and when to use events instead) is what keeps your components fast.

Why Livewire props aren't reactive by default#

Every Livewire component is independent. When something changes in the parent and a network request fires, only the parent's state is sent to the server and re-rendered — the child is skipped entirely. The child already exists as its own component on the page, so Livewire leaves it alone to keep the payload small. That's a deliberate performance decision, and most of the time it's the right one.

Here's the trap. A parent order-filter holds the selected status and passes it down to a child that shows a count:

<?php

use Livewire\Component;

new class extends Component {
    public string $status = 'all';

    public function setStatus(string $status): void
    {
        $this->status = $status;
    }
};
<div>
    <flux:button.group>
        <flux:button wire:click="setStatus('all')">All</flux:button>
        <flux:button wire:click="setStatus('paid')">Paid</flux:button>
        <flux:button wire:click="setStatus('refunded')">Refunded</flux:button>
    </flux:button.group>

    <livewire:order-count :$status />
</div>

And the child, order-count, receives $status and counts matching orders:

<?php

use App\Models\Order;
use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component {
    public string $status;

    #[Computed]
    public function total(): int
    {
        return Order::where('status', $this->status)->count();
    }
};
<flux:card>
    {{ $this->total }} orders
</flux:card>

Click "Paid" and the parent's $status flips to paid. The child's $status stays on all. The count never moves, because the child never got the memo. (If total looks unfamiliar, it's a memoised method — see Livewire 4 #[Computed(persist: true)] for caching heavy queries.)

Make a Livewire prop reactive with #[Reactive]#

Add the #[Reactive] attribute to the child property you want to track the parent. That's the whole fix:

<?php

use App\Models\Order;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Reactive;
use Livewire\Component;

new class extends Component {
    #[Reactive]
    public string $status;

    #[Computed]
    public function total(): int
    {
        return Order::where('status', $this->status)->count();
    }
};

Now when the parent updates $status, it sends the new value down to the child in the same response, and the child re-renders with it. No extra wiring on the parent side — the existing :$status binding is enough. Reactivity flows one way: parent to child.

This is the behaviour developers coming from Vue or React expect everywhere, and it's tempting to reach for it on every prop. Don't. Each reactive prop adds its value to the payload on every parent update, whether or not it changed. On a prop that's set once and never moves, that's pure overhead. Apply #[Reactive] only where the child genuinely needs to stay in sync — the filter above is a textbook case; a $title string passed at mount is not.

When to reach for events or $parent instead#

#[Reactive] couples the child tightly to one parent prop. For looser relationships — or when the child needs to react to an action rather than mirror a value — events are cleaner. The parent dispatches, the child listens:

public function setStatus(string $status): void
{
    $this->status = $status;

    $this->dispatch('status-changed', status: $status);
}
<?php

use Livewire\Attributes\On;
use Livewire\Component;

new class extends Component {
    public string $status = 'all';

    #[On('status-changed')]
    public function updateStatus(string $status): void
    {
        $this->status = $status;

        unset($this->total);
    }
};

Events decouple the two components — anything on the page can fire status-changed, and the child doesn't care who. That flexibility is the point, and it's also why events suit sibling-to-sibling or deeply nested updates that a direct prop can't reach. The cost is indirection: a listener with no dispatcher (or vice versa) fails silently.

There are two more tools worth knowing. #[Modelable] gives you two-way binding when you've extracted an input into its own component and want wire:model="status" on the child tag to read and write the parent's value — reach for it on form inputs, not display widgets. And if you're sharing purely client-side UI state, keep it in Alpine; share state with $wire and @entangle covers that without a server round-trip at all.

Gotchas and Edge Cases#

A few things that bite once #[Reactive] is in play:

  • You can't mutate a reactive prop in the child. Assigning to it — in an action, in mount(), anywhere — throws Cannot mutate reactive prop. Reactive props are a read-only channel from the parent. If the child needs to change the value, dispatch an event back up or call the parent directly with $parent.setStatus('paid').
  • Reactivity is one-directional. Parent to child only. A change made in the child never flows back to the parent through #[Reactive] — that's what events, $parent, and #[Modelable] are for.
  • Changing wire:key is the blunt alternative. Setting :wire:key="$status" on the child tag forces Livewire to destroy and recreate the child whenever the key changes. It "fixes" the stale value, but you lose all the child's internal state and pay a full re-mount. Use it when you want a clean slate, not as a substitute for #[Reactive].
  • Watch computed staleness. A plain #[Computed] re-runs each request, so it picks up the new reactive value automatically. But if you've added persist: true, the cached result won't budge until you unset() it — reactivity updates the prop, not your cache.
  • Maybe you don't need a child at all. If you only split this out to isolate re-renders, an island is simpler than a child plus a reactive prop. Livewire 4 islands give you scoped updates inside one component, no props or events required.

Wrapping Up#

If a child component is stuck on its first value, you're hitting Livewire's default: props don't react. Add #[Reactive] to the child property and the parent will push updates through on every change — just keep it off props that never move. When the relationship is looser than one parent feeding one child, dispatch an event instead. From here, the natural next steps are deciding islands versus nested components for isolated updates, and tidying multi-property components with Form Objects and #[Validate]. If you're still on v3, the Livewire 3 to 4 migration guide covers the SFC syntax used throughout this article.

FAQ#

Why is my Livewire child component not updating?

By default Livewire props are not reactive. When the parent changes a value and re-renders, only the parent's state is sent to the server — the child is skipped, so it keeps the value it received on first render. To make the child track the parent, add the #[Reactive] attribute to the relevant property in the child component, or push updates with an event.

What does the #[Reactive] attribute do in Livewire?

#[Reactive] marks a child component property so that it automatically updates whenever the parent passes a new value. With the attribute in place, the parent includes the prop's current value in its response and the child re-renders to match. It turns an otherwise static prop into a live, one-way binding from parent to child — similar to props in Vue or React.

Are Livewire props reactive by default?

No. Livewire props are static by default — the child receives the value once and ignores later changes from the parent. This is a deliberate performance choice that keeps each component independent and minimises the data sent over the wire. You opt into reactivity explicitly with #[Reactive], and only on the props that need it.

When should I use #[Reactive] versus events?

Use #[Reactive] for a tight parent-to-child relationship where the child mirrors a parent value, like a filter feeding a count. Use events when the coupling is looser — sibling components, deeply nested children, or cases where the child reacts to an action rather than a value. Events decouple sender and receiver at the cost of some indirection; reactive props are simpler but bind the child to one parent prop.

Why do I get a "Cannot mutate reactive prop" error?

Reactive props are read-only inside the child component. Assigning to one — in an action, in mount(), or anywhere else — throws Cannot mutate reactive prop, because the value is owned by the parent and only flows downward. To change it, dispatch an event the parent listens for, or call the parent action directly with the $parent magic variable, and let the new value flow back down.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.