React-final-form: initialValue of useField() hook resets input.value of the field

Created on 14 Jun 2019  路  10Comments  路  Source: final-form/react-final-form

Are you submitting a bug report or a feature request?

bug report

What is the current behavior?

const Component = () => {
  useField("firstName", { subscription: { value: true }, initialValue: "Max" });
  return (
    <div>
      <label>First Name (new)</label>
      <Field
        name="firstName"
        component="input"
        type="text"
        placeholder="First Name"
      />
    </div>
  );
};

When changing the input value of firstName Field to e.g. 'Max abc' and afterwards replacing this Component with an identical ComponentCopy (same fieldName and both using the useField() hook) the changed input.value gets reset to the initialValue: "Max" and therefore looses its changes.

Kapture 2019-06-14 at 16 20 41

What is the expected behavior?

Expected to work like <Form initialValues={{ firstName: "Max" }} ...> (which I added to the Sandbox demo at the bottom). When changing the input value of firstName Field to e.g. 'Max abc' and afterwards replacing this Component with an identical ComponentCopy (same fieldName, both use useField() hook to supply initialValue) the changed input.value (which is still part of the form) gets preserved and ComponentCopy displays the correct input.value 'Max abc' instead of the initialValue: "Max".

Kapture 2019-06-14 at 16 18 30

Sandbox Link

firstName demonstrates the bug and lastName presents a hacky solution to get the expected behavior.
At the bottom a standalone form demonstrates the expected behaviour by supplying the initialValues to the <Form .. directly which is at the moment not achievable by using the useField() hook.
https://codesandbox.io/s/react-final-form-simple-example-dmh3h?fontsize=14

What's your environment?

"final-form": "4.14.1",
"react-final-form": "6.2.0",
"node": ">=10.16.0",

Other information

Most helpful comment

// useField.js
import React from 'react';
import { useField as _useField, useForm } from 'react-final-form';

export const useField = (name, config) => {
  const form = useForm();
  const [alreadyRegistered] = React.useState(
    () => form.getFieldState(name) !== undefined,
  );

  if (alreadyRegistered) {
    // eslint-disable-next-line no-unused-vars
    let initialValue;
    // eslint-disable-next-line prefer-const, no-param-reassign
    ({ initialValue, ...config } = config);
  }

  return _useField(name, config);
};
// LabelEditor.js
import { useField } from './useField';
// ...
const { input } = useField(`${element.id}.label`, {
    type: 'text',
    subscription: { value: true },
    initialValue: get(element, 'state.label'),
  });

@Andarist thank you for your solution! I have tested it with our setup, and for now, it suffices as an intermediate workaround.

Nevertheless, I believe that fixing this issue at the useField() level as well as <Field .. level #536 would improve and simplify the UX of this remarkable form library.

All 10 comments

_fyi_ I have cleaned up the Sandbox example, updated the bug report and added improved gifs.

What u are asking here would make the code slightly more complicated than it needs to be. I would advise just using initialValues on the entire form - that way with initialValues values can be treated as preserved when changing rendering components OR when dealing with conditionally rendered components. And initialValue on the field itself might be treated as resetting the field. Both use cases are actually OK and both should be (somehow) supported.

Thank you, @Andarist, for your quick reply. What we try to achieve is a little bit more complicated.

  • Consider having a kind of form builder, and the user may have the option to add/create custom new fields to the form.
  • We want to initialise these new fields with predefined values (initialValue), whose values the user may later on change.
  • As the form elements may be repositioned, we have to delete and recreated them. Therefore if the input.value were altered before the field was moved, we would like to fill in this value after it got placed at a new position, otherwise set the initialValue.

tl;dr use case: add, on the fly, new fields (with initial values) to the form which later on may be changing rendering components.

As far as I have browsed the documentation, I was not able to find a trivial way to add new initial values to the initialValues object on the fly.

I tried to manipulate the initialValues object in the following manner [1], but it causes first off unnecessary rerendering, as well as resets the field which was initially set in the advised way by supplying its initialValue to initialValues.

That was my motivation to open this issue, because, in my opinion, should the useField() hook provide a non-resetting initialValue experience as does the initialValues while changing rendering components.

[1] https://codesandbox.io/s/react-final-form-simple-example-3b98e?fontsize=14

Kapture 2019-06-14 at 22 06 11

IMHO, in that case, you could just not render different component which controls the same named value, but rather just use useField inside a single component which in turn would render different component based on your conditions and returned field state.

I'm ofc not aware of your all constraints, so please describe the situation in more detail if you think that's not a suitable solution.

@Andarist most certainly this approach would be easy to accomplish. But the project does indeed have some constraints which don't allow us to implement it that way. Let me explain them.

  • The project may be described as a form builder (with different fields such as text input, checkbox, text-area, file upload ...).
  • The user may add new fields (with initialValues) within the form to expand/customise the form. The user may also edit the input values to adjust the form for a certain use case (e.g. contact form). 2019-06-14_23 57 53 Scan page 1
  • Each row is draggable and therefore allows to rearrange the form elements. If an element is dragged above another, they can be combined into one new two-column row element. Both initially full-width row fields are placed in a single full-width row component next to each other (e.g. first and last name). 2019-06-14_23 57 53 Scan page 2
  • Therefore we create a new two-column element with two new fields which inherit certain properties of the two fields which were initially combined. Both elements are now not draggable by themselves anymore, but instead the newly formed two column element which wraps the two fields acts a single row element and therefore is only draggable. We want to keep the initialValues / touched values of the user of prior to the combine event.
  • We provide an unlink button to restore the combined elements to their initial full-width row element expression. At this point, we again, destroy the two column element and its children and create two newly formed elements with inherited properties instead. We again want to keep the initialValues / touched values of the user of prior to the unlink event.

We are not able to use useField() to set the initialValue inside a single component, because of the reordering of the row elements, as well as combining and unlinking of certain fields is possible and thus change rendering components is needed. Setting initialValues on the Form element is not possible as well, as we don't know which fields the user creates in the process.

I hope I was able to explain our constraints somewhat clearly.

You have explained the problem really well! I feel like this could be supported - but I'm going to leave that decision up to @erikras .

By stretching a little bit current APIs I've managed to prepare a working solution for you though - https://codesandbox.io/s/react-final-form-simple-example-g9gqf - which can be used as an intermediate workaround.

I got the similar problem but with destroy on unregister https://github.com/final-form/react-final-form/issues/523

With two fields it's harder to stop this issue but it's more visible with enabled destroy on unregister property. I quickly looked through code and found that to get initial value for for field you need to registerField and then immediately do unregister

// useField.js
import React from 'react';
import { useField as _useField, useForm } from 'react-final-form';

export const useField = (name, config) => {
  const form = useForm();
  const [alreadyRegistered] = React.useState(
    () => form.getFieldState(name) !== undefined,
  );

  if (alreadyRegistered) {
    // eslint-disable-next-line no-unused-vars
    let initialValue;
    // eslint-disable-next-line prefer-const, no-param-reassign
    ({ initialValue, ...config } = config);
  }

  return _useField(name, config);
};
// LabelEditor.js
import { useField } from './useField';
// ...
const { input } = useField(`${element.id}.label`, {
    type: 'text',
    subscription: { value: true },
    initialValue: get(element, 'state.label'),
  });

@Andarist thank you for your solution! I have tested it with our setup, and for now, it suffices as an intermediate workaround.

Nevertheless, I believe that fixing this issue at the useField() level as well as <Field .. level #536 would improve and simplify the UX of this remarkable form library.

Fix published in [email protected].

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mewben picture mewben  路  3Comments

morloy picture morloy  路  4Comments

antoinerousseau picture antoinerousseau  路  3Comments

mvoloskov picture mvoloskov  路  4Comments

CodeWithOz picture CodeWithOz  路  4Comments