livewire/livewire: Fix resetting uninitialized form properties
Contributed by calebporzio
Part of the livewire v4.4.7 release
This patch fixes a crash in Livewire's Form objects. On v3.8.10, calling reset() on a form that declares a required typed property without a default — for example public string $title; — threw a TypeError, because reset read the property's missing default as null and then tried to assign that null back onto the non-nullable property. PR #10593 changes Form::reset() so those properties are instead restored to their declared, uninitialized state. If you're upgrading from v3.8.10, strictly-typed forms can finally be reset; the only follow-up you may need is an explicit default for fields you read or render immediately after resetting.
What changed
Livewire\Form::reset() previously created a fresh instance of the form class and copied each requested property back onto the live form:
data_set($this, $property, data_get($freshInstance, $property));
That round-trip breaks for required typed properties, because a fresh instance never initializes them. The fix adds a branch before that line: if the requested name is a direct property (it contains no dot), exists on the form class, and is uninitialized on a fresh instance, reset now calls data_forget() to un-set the property on the live form instead:
// Essence of the change in Form::reset()
if (! $property->contains('.') && property_exists($freshInstance, (string) $property)) {
if (! (new \ReflectionProperty($freshInstance, (string) $property))->isInitialized($freshInstance)) {
data_forget($this, (string) $property); // back to its declared uninitialized state
continue;
}
}
The change covers full resets, single-property resets, and the render that follows a reset action in the browser. It is verified by the full unit suite (1,394 tests, 3,972 assertions), the Form object suite (53 tests, 296 assertions), and a new browser regression test confirming that a reset action completes and the next render observes the property as uninitialized.
Why it matters
Typed properties without defaults are a common and reasonable way to declare required form fields:
class PostForm extends Form
{
#[Validate('required|string|max:255')]
public string $title; // no default: uninitialized until assigned
}
On v3.8.10, the standard "save, then reset" pattern would explode inside reset() itself:
public function save()
{
$this->form->store();
$this->form->reset();
// v3.8.10: TypeError — Cannot assign null to property PostForm::$title of type string
}
The root cause is a semantic mismatch: for such a property there is no "default value" to copy — its declared state is uninitialized. Reading that state as null and writing it back violates the property's type. Resetting should restore the state the form class actually declared, and that's exactly what this patch does.
What stays the same
Reflection is deliberately limited to direct properties, so dotted nested paths keep their existing behavior. A call like $this->form->reset('profile.name') still travels the data_get/data_set path and restores the declared default from the form class:
class ProfileForm extends Form
{
public array $profile = ['name' => ''];
}
$this->form->reset('profile.name'); // resets back to '' — unchanged
Properties that do have defaults — nullable properties (public ?string $subtitle = null;), strings, arrays, anything with an initializer — are also unchanged: reset restores the declared default exactly as before. Single-property resets preserve sibling values; resetting 'title' leaves $content untouched.
Upgrade impact
For forms where every property is nullable or has a default, nothing changes — no action required. The impact is concentrated in two places:
1. Resets that used to crash now work. If you previously worked around the TypeError (or avoided reset() entirely on strictly-typed forms), you can remove the workaround.
2. Reading right after reset is now an explicit choice. After reset(), a property without a default is uninitialized. Reading it directly throws an Error — not a TypeError:
$this->form->reset();
$this->form->title; // Error: Typed property PostForm::$title must not be accessed before initialization
If a field needs to be read or rendered immediately after resetting, the updated forms.md documentation recommends giving it an explicit default:
public string $title = '';
public ?string $subtitle = null;
Alternatively, assign the property after calling reset() and before reading it. Neither is required if you simply don't touch the property until a value is set again — the browser regression test demonstrates that the reset action and the following render complete cleanly while the property sits in its uninitialized state.
Takeaways
Form::reset()no longer throws aTypeErrorfor required typed properties likepublic string $title;; it restores them to their uninitialized declared state viadata_forget().- Direct properties, single-property resets, and full resets are covered; dotted paths such as
reset('profile.name')keep their existingdata_get/data_setbehavior. - Properties with defaults (including nullable and array defaults) behave exactly as on v3.8.10.
- If you read or render a field immediately after
reset(), give it an explicit default (= '',= null) or assign it before reading — the docs now say so explicitly. - Ships with full unit, Form object suite, and browser regression coverage, plus a JavaScript bundle build check.