React-hook-form doesn't set isDirty to false when back to initial state

In the official example for controlled inputs , if you modify the value of an input and then change it back to its initial value, isDirty will be set to true but won't be set back to false and dirtyField will contain the touched field.

In an older example with uncontrolled inputs, we don't have quite same behaviour. In fact, if you modify the value of an input and then change it back to its initial value, isDirty will still be falsy but dirtyFields will not contain the touched field.

Shouldn't isDirty be set back to false when the form is back to its initial state and dirtyFields should be empty?

Is it the intended behaviour?

Does the Controllers break the formState?

Upvotes: 26

Views: 68410

Answers (4)

Bret Weinraub
Bret Weinraub

Reputation: 2283

This may be helpful if other recipes don't lead you to the correct solution.

At issue is when you want to initialize a form with data that is not available when the encapsulated react component is loaded, for example when the source data is from an API.

In this case, useForm must be used before the default value is available, so the documentation that you see that looks like this:

This might not work if your source data isn't available right away

const {
  register,
  handleSubmit,
  watch,
  formState: { errors, isDirty, dirtyFields },
  setValue,
  reset,
} = useForm<YourDataType>({
  defaultValues: {
    ... // your fields go here
  },
});                  

That's not going to work because your default data won't be available until the API call returns, which is after the component initializes.

The solution (at least for me), is to use useEffect on the source data to reset the defaultValues once the data is available.

In my case I am working with a registration object

Solution Reset Your Default Data When The Source Becomes Available

(use same snippet from above plus)

useEffect(() => {
  if (yourObject) {
    reset({
      // your data
    });
  }
}, [yourObject, reset]);

If isDirty just isn't working for you, then it very may well be that the defaultData isn't available when you called useForm.

Upvotes: 0

Jarrod McGuire
Jarrod McGuire

Reputation: 683

Two years late, but adding to what Bill answered and if anyone has a similar issue (like I did). Not only does it need default values on the useForm, there can't be additional properties on the default values object that aren't on the form. The issue I had was that my form took a merge of the default values (for a new entry) and the data object (for editing an existing entry). So if you had something like this

const person = {id: 123, firstName: "Jane", lastName: "Smith"};

and your form was used for editing and creating, the component could have a optional prop for "person" and useForm is setup like this

const formMethod = useForm({
  defaultValues: person || {firstName: "", lastName: ""},
});

I didn't add the "id" property to the form, because that value wasn't ever going to be edited and you don't have an id for create. The "id" value is available for the update method outside of the form and can be patched into the submit values. Maybe that's an anti-pattern, but it's what was done.

When the form is first rendered, isDirty will be false. When you change a value, isDirty will be true. When you change back to the original value, isDirty remains true, because ALL of the default values don't match the form input values and never will in this instance.

This is a simplistic example where the id could be added to the form as a hidden input or registered or something, but for a real solution, there could be lots of other fields that are coming down the api that aren't editable and shouldn't (or couldn't) be part of the form. Takeaway is this - make sure that your editing model matches your default values model. I used this to merge the objects, remove nulls, patch in missing props and remove props that weren't on the default. "mergeWith" and "pick" are lodash methods.

function mergeFormData(defaults: Object, data?: Object, removeExtras?: boolean) {
    if (!data) return defaults;

    // This should merge everything on data, and take
    // defaults where applicable.
    // It's only shallow merge, and it uses the "data" if it isn't null or undefined.
    // ?? should do it, so it will allow "" and 0 to be kept on the data object
    // Use removeExtras to take out props from the data object
    // that aren't on the default object

    return mergeWith({},
        removeExtras ? pick(data, Object.keys(defaults)) : data,
        defaults,
        (a, b) => {return a ?? b;});
}

And usage (in my case) for add/edit.

const formContext = useForm<AddressFormData>({
    defaultValues: mergeFormData(defaultAddress, address, true),
    resolver: yupResolver(addressSchema),
    mode: 'onBlur'
});

Upvotes: 18

Parth Dave
Parth Dave

Reputation: 27

Ensure to subscribe to isDirty field from useForm then it will work.

const { handleSubmit, control, formState: { isDirty }, } = useForm({ defaultValues: baseRecord, });

Upvotes: -1

Bill
Bill

Reputation: 19268

isDirty is based on the form input values against default values.

https://react-hook-form.com/api#formState

Make sure to provide all inputs' defaultValues at the useForm, so hook form can have a single source of truth to compare whether the form is dirty.

Here is an example with uncontrolled inputs: https://codesandbox.io/s/bold-kapitsa-7m6o0?file=/src/App.tsx

example with controlled inputs: https://codesandbox.io/s/dark-framework-op8jy?file=/src/App.tsx

Upvotes: 13

Related Questions