Skip to content

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

Description

@StevenHensgen

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 }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions