When a Livewire component keeps an Eloquent model in a public property, what goes into the snapshot and what happens on the next request?
answer
- class alias and key, nothing else
- re-queried with firstOrFail each request
- unsaved attributes and select() are lost
- lazy proxy on PHP 8.4
- morphMap hides the class name
basics
~20 sOnly the model's class, or its morph-map alias, and primary key go into the Livewire snapshot. On each later request Livewire re-queries the row with firstOrFail, so unsaved attribute changes, loaded relations and select() constraints are gone.
solid answer
~40 sLivewire's model synthesizer dehydrates a model to its class, or a `Relation::morphMap()` alias, plus its key; attributes never enter the snapshot. On the next request it rebuilds the model from that signed meta: it reuses an instance already bound this request, for example by the route's `SubstituteBindings`, and otherwise runs `newQueryForRestoration($key)->useWritePdo()->firstOrFail()`, lazily on PHP 8.4+. Consequences: edits made to `$this->payslip->amount` without `save()` vanish between requests, eager-loaded relations and `select()` columns are not restored, a deleted row turns the next click into a 404, and each request pays a query. The upside is security: the client cannot swap the model or write its attributes directly. Keep editable fields in scalar properties or a form object.
go deeper
Remember that a model in a public property is re-fetched from the database on every Livewire request, so unsaved changes on it do not survive.
Explain dehydration to class and key, rehydration with firstOrFail from the write connection, and what is lost: dirty attributes, relations and select constraints.
Design around it: model for identity, scalars or a form object for edits, computed properties for queries, morph maps for class names, and 404 handling for stale pages.
Weigh the per-request query cost and stale-page behaviour of model properties against the tamper-proof identity they give, and set a team default.
## How Livewire stores a model Livewire persists a component's **public properties** by *dehydrating* them into a JSON snapshot after each request and *hydrating* them back on the next one. Values JSON cannot represent, such as Eloquent models, collections and enums, go through **synthesizers**. For a model, `ModelSynth` writes: - `class`: the model class, or its alias if one is registered with `Relation::morphMap()`; - `key`: the primary key, only when the model exists in the database. That is all. No attributes, no dirty state, no loaded relations. The Livewire docs show a `relationships` entry in older examples; the pinned 4.4.7 source writes only class and key. A new, unsaved model dehydrates with no key and comes back as an empty `new $class`. ## What hydration does On the next request Livewire verifies the snapshot's checksum, then rebuilds the property: 1. Resolves the class, following the morph map, and refuses anything that is not a `Model` subclass. 2. Reuses an instance **already resolved in this request**, for example the `{payslip}` bound by the page route's `SubstituteBindings` middleware, which Livewire re-applies as persistent middleware, as long as that instance is still clean and exists. 3. Otherwise runs `(new $class)->newQueryForRestoration($key)->useWritePdo()->firstOrFail()`, which reads from the write connection so a replica lag cannot hand back stale data. 4. On **PHP 8.4 and later**, wraps that query in a native **lazy proxy**, so the query runs only when code first touches the model; on older PHP it runs immediately. ## Consequences | Situation | Result on the next request | |---|---| | `$this->payslip->amount = 500;` without `save()` | Change lost; the row is re-read | | Model loaded with `->load('lines')` | Relation not restored; accessing it lazy-loads again | | Collection built with `->select(['id', 'total'])` | Constraint lost; full rows come back | | Row deleted by another user | `ModelNotFoundException`, rendered as 404 | | Every request | One query per model property, unless reused or never touched | The Livewire docs recommend **computed properties** for queried data, because a `#[Computed]` method re-runs your exact query each request instead of relying on hydration to reproduce it. ## Collections behave differently An Eloquent collection in a public property goes through `EloquentCollectionSynth`, which stores the collection class, the model class and the **list of keys**. On hydration it reuses already-resolved models and fetches the rest in **one** query with `get()`, not `firstOrFail()`, then rebuilds the collection in the original key order. A row deleted in the meantime is not an error: it is silently **dropped** from the collection. So a single model property fails loudly on a stale page, while a collection property quietly shrinks. Both lose `select()` columns, `with()` relations and any ordering or filtering that is not already captured by the key list. ## The security side Because the class and key come from the **signed** snapshot, the client cannot point the property at another row. An update that targets the property is hydrated from the snapshot's meta, and the client's value is ignored. A deep write such as `wire:model="payslip.amount"` throws, since the synthesizer refuses to set model attributes directly. That behaviour changes only if you turn on `legacy_model_binding` in `config/livewire.php` (default `false`), the Livewire 2 style of binding inputs to model attributes with validation rules. Two things still leak: - **The class name**, such as `App\Models\Payroll\Payslip`, unless a morph map alias is set. - **The key**, which is harmless for opaque keys but reveals counts and ordering for auto-increment ids. ## Patterns that follow 1. Hold the model for identity, and copy editable fields into scalar properties or a `Livewire\Form` object in `mount()`; write them back in the save action. 2. Load related data in a computed property or in `render()`, not by eager-loading onto the stored model. 3. Register a morph map if class names are sensitive in your domain. 4. Expect a 404 on stale pages after deletes, and handle it in the UX if that matters.
- Why does a Livewire component using route model binding not query the model twice on each update?Livewire re-runs the page route's `SubstituteBindings` as persistent middleware, which binds `{payslip}` again. It records the bound models, and the model synthesizer reuses an instance of the same class and key as long as it still exists and is not dirty, instead of issuing its own `firstOrFail()` query.
- What does legacy_model_binding change in Livewire 4?With `legacy_model_binding` set to `true` in `config/livewire.php`, Livewire registers its older model synthesizers, which allow binding inputs to model attributes such as `payslip.amount` as long as a validation rule exists for them. The default is `false`, where writing a model attribute from the client throws.
saying these in an interview costs you the question
- Livewire keeps the whole model with its attributes in the snapshot.
- Unsaved changes to a model property survive until the next save().
- Eager-loaded relations stay loaded between Livewire requests.
- The client can change a model property's id if it knows another id.
- Livewire keeps the model in server memory between requests.