livewire/livewire: Fix reactive Eloquent models throws CannotMutateReactivePropException when relations are lazy-loaded
Contributed by ghabriel25
Part of the livewire v4.4.7 release
Livewire 4's reactive props feature had a sharp edge: a child component that merely read a relation on a reactive Eloquent model prop — including triggering a lazy load — could crash with CannotMutateReactivePropException, even though it never wrote to the prop. PR #10536 fixes this by changing how reactive props detect mutation. If you're upgrading from v3.8.10, reactive props (#[Reactive]) are entirely new in v4, and this fix makes their mutation guard consistent with how Livewire already treats models everywhere else: identity and attributes matter, loaded relations do not.
What changed
Previously, BaseReactive detected prop mutations with a single hashing strategy at mount, hydrate, and dehydrate:
crc32(json_encode($this->getValue()));
For Eloquent models, json_encode() serializes loaded relations too. This PR replaces those inline hashes with a shared hasher on SupportReactiveProps:
- Models are hashed by their morph-map-aware class alias, primary key, and
getAttributes()— never their loaded relations. - Everything else still uses
crc32(json_encode($value)), but now returnsnullwhen encoding fails instead of silently hashingfalse(previously, two unencodable values likeNANorINFcould collide on the same hash and falsely compare as equal). - Hashes are cast to strings, because
crc32()can return negative integers on 32-bit PHP builds.
A hashesMatch() helper ties this together and is used when comparing current versus incoming values during hydrate. The update-skip logic (shouldSkipUpdate() / modelMatchesSnapshot()) is unchanged and remains identity-based, matching ModelSynth, which only dehydrates a model's class and key.
Why it matters
Consider the most ordinary render pattern imaginable:
#[Reactive]
public Article $article;
public function render()
{
return view('...', [
'author' => $this->article->author->name, // lazy-loads the relation
]);
}
In Eloquent, accessing $this->article->author lazy-loads the relation and mutates the in-memory model by attaching it. That mutation had nothing to do with the prop's data — but it changed the json_encode hash, so on dehydrate Livewire threw CannotMutateReactivePropException. In practice this made reactive model props unusable unless you eager-loaded every relation from the parent, and it made mutation detection stricter than the rest of the model pipeline, which deliberately ignores relations when moving models between parent and child.
The new mutation rules
The reactive contract — "a child must not mutate the prop" — is preserved, just scoped to what actually matters:
// ✅ Safe: reading relations (lazy or eager loaded)
$this->article->author->name;
// ❌ Still throws CannotMutateReactivePropException:
$this->article->title = 'Changed'; // attribute write
$this->article = Article::find(2); // different model identity
You might wonder why attributes are hashed at all instead of relying on isDirty() / wasChanged(). The PR notes explain: a parent may hand the child an already-dirty model, and those flags would false-positive after setValue. Comparing an attribute snapshot from hydrate to dehydrate is what catches real writes without punishing inherited dirty state.
Upgrade impact
If you're coming from v3.8.10, there is nothing to migrate — reactive props are a v4 feature, and you're picking up this fix as part of adopting them. Two practical notes:
- If you experimented with earlier v4 builds and worked around this bug (eager-loading every relation in the parent, wrapping renders in try/catch, or avoiding model props entirely), those workarounds can be removed.
- The exception's scope has narrowed: it now fires only on attribute mutations or identity changes. That is almost certainly what you want, but if any of your code depended on the exception firing for relation reads, it won't anymore.
One more subtlety worth knowing: because the model's class is hashed via its morph alias when one is registered, hashes stay stable in applications that use aliased morph maps.
Known limitations
The PR author is explicit that two cases are not fixed, pending a decision on whether they should be supported:
- Reactive collections / arrays of models still hash via a full
json_encode(), so the original false-positive can still occur when relation reads happen on items inside the collection. - Custom objects implementing
JsonSerializablethat embed models are also still strict.
If your reactive props are plain models or scalars, you're covered; if they're arrays or wrapped objects, keep eager-loading defensively for now and watch the linked discussions (#6090, #6033) for where this lands.
Takeaways
- Reading relations on a reactive Eloquent model prop — lazy-loaded or eager-loaded — no longer throws
CannotMutateReactivePropException. - Attribute mutations and model identity swaps still throw; the "children must not mutate reactive props" contract is intact.
- Non-model values behave as before, with safer handling of values that fail
json_encode()and 32-bit-safe hash casting. - Collections/arrays of models and
JsonSerializablewrappers remain affected; don't rely on lazy loads inside them yet. - No configuration or code changes are required to upgrade — the fix is transparent.