Bug Report
Version
react-final-form: 7.0.1
final-form: 5.0.1
react: 19.0.6
Description
The StrictMode fix added in v7.0.1 (PR #1068/#1069, fixing #1031) introduced a regression where useField overwrites existing form values when a field is registered at a path that was never previously registered as its own field — even when destroyOnUnregister is false (the default).
This breaks multi-step/wizard forms where different steps register useField at different paths within the same form state tree.
Reproduction
- Step 1 component registers
useField("parent") and calls form.change("parent.child.value", data) to set a nested value
- User advances to step 2 — step 1 unmounts, step 2 mounts
- Step 2 component registers
useField("parent.child.value") to read the value
- Expected:
useField returns data (the value set in step 1)
- Actual:
useField overwrites it with initialValues.parent.child.value, discarding data
Root Cause
The fix added in v7.0.1 prepends this logic to useField's useEffect:
var existingFieldState = form.getFieldState(name);
if (!existingFieldState) {
var formInitialValue = getIn(formState.initialValues, name);
var valueToSet = formInitialValue !== undefined ? formInitialValue : initialValue;
if (valueToSet !== undefined) {
form.change(name, valueToSet);
}
}
The problem is that getFieldState(name) checks whether a field is registered at that exact path, not whether a value exists in the form state at that path. When a value was set via change() through a parent field (or directly on the form), no field registration exists at the nested path, so getFieldState returns null, and the code overwrites the existing value with initialValues.
This code was not present in v7.0.0 — in that version, useEffect simply called register() without the getFieldState check.
Suggested Fix
Before calling form.change(), check whether the form's current values already differ from initialValues at that path:
if (!existingFieldState) {
var formState = form.getState();
var currentValue = getIn(formState.values, name);
var formInitialValue = formState.initialValues ? getIn(formState.initialValues, name) : undefined;
var valueToSet = formInitialValue !== undefined ? formInitialValue : initialValue;
// Only reset if no value has been set via change()
if (valueToSet !== undefined && currentValue === undefined) {
form.change(name, valueToSet);
}
}
Workaround
Set the initial value for affected fields to undefined instead of null in initialValues. This causes valueToSet to be undefined, which skips the form.change() call entirely:
// Before (broken): initialValues triggers overwrite
{ Data: null }
// After (workaround): undefined skips the form.change() call
{ Data: undefined }
Bug Report
Version
react-final-form: 7.0.1final-form: 5.0.1react: 19.0.6Description
The StrictMode fix added in v7.0.1 (PR #1068/#1069, fixing #1031) introduced a regression where
useFieldoverwrites existing form values when a field is registered at a path that was never previously registered as its own field — even whendestroyOnUnregisterisfalse(the default).This breaks multi-step/wizard forms where different steps register
useFieldat different paths within the same form state tree.Reproduction
useField("parent")and callsform.change("parent.child.value", data)to set a nested valueuseField("parent.child.value")to read the valueuseFieldreturnsdata(the value set in step 1)useFieldoverwrites it withinitialValues.parent.child.value, discardingdataRoot Cause
The fix added in v7.0.1 prepends this logic to
useField'suseEffect:The problem is that getFieldState(name) checks whether a field is registered at that exact path, not whether a value exists in the form state at that path. When a value was set via change() through a parent field (or directly on the form), no field registration exists at the nested path, so getFieldState returns null, and the code overwrites the existing value with initialValues.
This code was not present in v7.0.0 — in that version,
useEffectsimply calledregister()without thegetFieldStatecheck.Suggested Fix
Before calling
form.change(), check whether the form's current values already differ from initialValues at that path:Workaround
Set the initial value for affected fields to
undefinedinstead ofnullin initialValues. This causesvalueToSetto beundefined, which skips theform.change()call entirely: