jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
34.01k stars 2.79k forks source link

setFieldValue to undefined deletes the value from values and prevents errors from showing / touched being updated on submit #2332

Open amir-virta opened 4 years ago

amir-virta commented 4 years ago

🐛 Bug report

Current Behavior

When setFieldValue() is called to set a field to undefined, Formik deletes it from values. The consequence of this is that Formik no longer marks the field as touched on submit and therefore error messages for the field no longer show (using <ErrorMessage>)

Steps to repro: 1) Formik state after submit is clicked and the input field is not changed by the user. As you can see the error message is visible and touched is automatically set.

image

2) Formik state after (a) input field is changed to a value, then (b) the input field is cleared, and then (c) submit is clicked again. The nuance here is that when the field is cleared, setFieldValue is called to set the form field to undefined. You can see the error exists, but the field name is no longer in values and touched does not have the field name either.

image

Expected behavior

On submit, Formik should continue to mark ALL the keys in initialValues as touched.

Reproducible example

https://codesandbox.io/s/formik-setfieldvalue-deletes-the-value-from-values-and-prevents-errors-from-showing-on-submit-ysxdh?fontsize=14&hidenavigation=1&theme=dark

Suggested solution(s)

Two potential solutions (a) Formik should set the field value to undefined instead of deleting it from values when setFieldValue is called with the value of undefined OR (b) Formik should clone the keys from initial values and use that to determine which fields to touch on submit instead of the current set of keys in values.

Additional context

None

Your environment

Software Version(s)
Formik 2.1.1 (and 1.1.1)
React 16.12.0
TypeScript n/a
Browser Chrome Version 80.0.3987.122 (Official Build) (64-bit)
npm/Yarn Yarn
Operating System Mac OS X
justinbhopper commented 4 years ago

I agree, formik should be setting the value to undefined, not deleting the property from the values object. Many operations in Formik rely on the existence of the entry in the values object in order to know what is errored/touched, and by deleting, it is wiping that data.

These lines seem to be the culprits, although there may be others: https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/utils.ts#L131 https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/utils.ts#L139

clausreinke commented 4 years ago

Those two lines lead to #781, which attempts to fix #727. The PR title suggests this is intended behaviour for setIn, but it seems the original issue was only about setFieldError, where it makes sense for the keys to be removed.

Was it really the intention to have setFieldValue be affected by this?

circAssimilate commented 4 years ago

Thanks for filing this, I suspected that this "feature" we were leaning on was actually a bug! Fwiw: I created https://github.com/formik/formik/issues/2598 to backfill this functionality.

MatejFacko commented 3 years ago

Guys, any update on this? Seems to me that the issue is still there.

justinbhopper commented 3 years ago

For anyone who has run into this issue and needs a workaround, one solution is to create your own wrapper around Formik's setFieldValue function to override its behavior. You can do this by either wrapping Formik entirely (which is more work but allows for more seamless integration) or just calling a surrogate function manually.

Here is what a surrogate function would look like:

function setFieldValue(formik: Formik<Values>, field: string, value: any, shouldValidate?: boolean): void {
  // Override default behavior by forcing undefined to be set on the state
  if (value === undefined) {
    const values = setIn(formik.state.values, field, undefined);
    formik.setValues(values);

    const willValidate = shouldValidate === undefined ? this.props.validateOnChange : shouldValidate;
    if (willValidate) {
      formik.validateForm(values);
    }
  } else {
    // Use default behavior for normal values
    formik.setFieldValue(field, value, shouldValidate);
  }
}

/**
 * A copy of Formik's setIn function, except it assigns undefined instead of deleting the property if the value
 * being set is undefined.
 * https://github.com/jaredpalmer/formik/issues/2332
 */
function setIn(obj: any, path: string, value: any): any {
  const res: any = _.clone(obj); // This keeps inheritance when obj is a class
  let resVal: any = res;
  let i = 0;
  const pathArray = _.toPath(path);

  for (; i < pathArray.length - 1; i++) {
    const currentPath: string = pathArray[i];
    const currentObj: any = getIn(obj, pathArray.slice(0, i + 1));

    if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {
      resVal = resVal[currentPath] = _.clone(currentObj);
    } else {
      const nextPath: string = pathArray[i + 1];
      resVal = resVal[currentPath]
        = isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
    }
  }

  // Return original object if new value is the same as current
  if ((i === 0 ? obj : resVal)[pathArray[i]] === value) {
    return obj;
  }

  if (value === undefined) {
    resVal[pathArray[i]] = undefined;
  } else {
    resVal[pathArray[i]] = value;
  }

  // If the path array has a single element, the loop did not run.
  // Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.
  if (i === 0 && value === undefined) {
    res[pathArray[i]] = undefined;
  }

  return res;
}
zyofeng commented 3 years ago

For anyone who has run into this issue and needs a workaround, one solution is to create your own wrapper around Formik's setFieldValue function to override its behavior. You can do this by either wrapping Formik entirely (which is more work but allows for more seamless integration) or just calling a surrogate function manually.

Here is what a surrogate function would look like:

function setFieldValue(formik: Formik<Values>, field: string, value: any, shouldValidate?: boolean): void {
  // Override default behavior by forcing undefined to be set on the state
  if (value === undefined) {
    const values = setIn(formik.state.values, field, undefined);
    formik.setValues(values);

    const willValidate = shouldValidate === undefined ? this.props.validateOnChange : shouldValidate;
    if (willValidate) {
      formik.validateForm(values);
    }
  } else {
    // Use default behavior for normal values
    formik.setFieldValue(field, value, shouldValidate);
  }
}

This doesn't work as setIn is the one that removes the obj if value is undefined. see line 130

https://github.com/formium/formik/blob/2d613c11a67b1c1f5189e21b8d61a9dd8a2d0a2e/packages/formik/src/utils.ts

justinbhopper commented 3 years ago

@zyofeng You're right, my apologies. I failed to paste the important part, which was a replacement of setIn that I was using that changes the default setIn behavior. I've updated my original comment to include it.

Kepro commented 3 years ago

This is an annoying issue, I spent an hour figuring out the issue, and it's just because if a value is undefined, Formik just ignores it... like Formik set error but touched is false... also for submit... I must add null or empty strings to initialValues to start showing errors

carlosthe19916 commented 3 years ago

Using null or empty strings also worked for me but I think this should be considered just as a workaround since undefined values shouldn't be removed from values

Kepro commented 3 years ago

also, this is the issue when using initialValues and you don't use all values, user click "submit" and we never show error on fields that are emptym because of undefined

duc-tm commented 1 year ago

Still unresolved in 2023?

DarkPage commented 1 year ago

I got the same problem. The onChange function set field value to undefined. And then the form shows no error message. Because the errors and values object has delete the field key. 2023-11-02 I am crazy.

sylvaindubus commented 1 year ago

Same problem here, and it took a while to figure out that my issue was this :(

tlmader commented 11 months ago

Just ran into this issue myself. Had to work around it by using setValues:

  const handleClear = (name: keyof FormValues) => () => {
    /**
     * setFieldValue to undefined deletes the value from values
     * https://github.com/jaredpalmer/formik/issues/2332
     */
    setValues((previous) => ({
      ...previous,
      [name]: undefined,
    }));
  };
souvik1000 commented 7 months ago

This issue is still there. On clearing the selection, once we try to set the value as undefined, on submit we are not getting the value with the version of "^2.1.5". So, as a work around, currently I'm overlapping the current outcome with the initialValue. Cause, if some value is not coming, I'm expecting that, it will be undefined only.

const initialCustomerFilterValues = {
    customerStatus: undefined,
    organizationStatus: undefined
}

<Formik
    initialValues={initialCustomerFilterValues}
    onSubmit={(values) => {
        filterHandler({ ...initialCustomerFilterValues, ...values })
   }}
>
    ...
</Formik>