final-form / final-form/react-final-form

useField v7.0.1 overwrites form values set via change() for previously unregistered field paths

Open
#1,095 4 comments 8 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
7.4k
Forks
497
PR merge metrics
No merged PRs in 30d

Description

## 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

1. Step 1 component registers `useField("parent")` and calls `form.change("parent.child.value", data)` to set a nested value
2. User advances to step 2 — step 1 unmounts, step 2 mounts
3. Step 2 component registers `useField("parent.child.value")` to read the value
4. **Expected:** `useField` returns `data` (the value set in step 1)
5. **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 }
```

Contributor guide

Open the contributing guide

Research direction

Start at useField's useEffect and compare the StrictMode changes from PRs #1068 and #1069. Reproduce the multi-step sequence where form.change() sets a nested value before the field registers; done means the later useField preserves that value instead of replacing it with initialValues.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.