jaredpalmer / formik

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

Touched state when using FieldArrays #3344

Open james-portelli-cko opened 3 years ago

james-portelli-cko commented 3 years ago

Bug report

The ISSUE

Im running into an issue displaying validation errors for FieldArrays using Formik and Yup.

Ideally, to provide a good user experience we would want to show errors related to a field only when the field has been touched by the user, and while this is quite easy to accomplish with normal fields, Im struggling to mimic the behaviour for FieldArrays.

The reality is that for FieldArrays the ‘touched’ state doesn’t match up well with the way errors can be raised by YUP validations.

There are two issues:

  1. Formik will only manage the 'touched' state of items within the array but not the array itself so if you had a validation on the min and max amount of items on the array and the user removed all the items there will be no touched state for the array, but there will be an error.

  2. FieldArray helpers dont update the touched state of an array when either 'pushing' or 'removing' items from the array.

The below is an example which demonstrates the above two issues:

With the following schema we expect there to be at least 1 item within ‘someArray’ but no more than 5 items.

    Yup.object({
        someArray: Yup.array().min(1).max(5)
    })

Imagine the following initial formik state, at this point we wouldn’t want to show the someArray error because nothing has been touched.

    initialValues: {
        someArray: []
    },
    values: {
        someArray: []
    },
    errors: {
        someArray: ‘At least 1 item should be in this array’
    },
    touched: {}

If I ‘push’ and item to someArray, then it will not longer be an error and I would expect the following state:

    initialValues: {
        someArray: []
    },
    values: {
        someArray: ['item']
    },
    errors: {
        someArray: undefined
    },
    touched: {
        someArray: [true]
    }

Unfortunately when using the FieldArray helpers the 'touched' state isnt updated, so the actual state we would get is:

    initialValues: {
        someArray: []
    },
    values: {
        someArray: ['item']
    },
    errors: {
        someArray: undefined
    },
    touched: {}

If I 'remove' the item added now, then the error will return and I would expect the following state:

    initialValues: {
        someArray: []
    },
    values: {
        someArray: []
    },
    errors: {
        someArray: ‘At least 1 item should be in this array’
    },
    touched: {
        someArray: true
    }

Unfortunately again the FieldArray helpers don't update the 'touched' state. As a result this error wont be shown to the user (provided im only showing the error is the field was touched)

    initialValues: {
        someArray: []
    },
    values: {
        someArray: []
    },
    errors: {
        someArray: ‘At least 1 item should be in this array’
    },
    touched: {}

Expected behavior

Reproducible example

https://codesandbox.io/s/upbeat-shamir-hmiwq?file=/src/Users.tsx

Your environment

Software Version(s)
Formik 2.2.9
React 17.0.2
TypeScript 4.3.5
Browser Chrome
npm/Yarn Yarn
Operating System macOs Catalina 10.15.7
james-portelli-cko commented 3 years ago

@jaredpalmer don't suppose you could have a quick look as I'm really at a loss on how to solve this one.

johnrom commented 3 years ago

In my mind, the solution is when an array has been touched but has no values, it should just be an empty array [].

james-portelli-cko commented 3 years ago

Thanks for getting back to me @johnrom

I suppose that would also work, because we would differentiate between an array property being 'undefined' (not touched) or an '[]' (touched) in the touched object to show the errors.

Is there any reason the FieldArray helpers do not affect the touched status by default?

johnrom commented 3 years ago

I don't have any idea, unfortunately.

james-portelli-cko commented 3 years ago

No worries @johnrom, any idea who could pitch in on this one?

as-zlynn-philipps commented 2 years ago

@james-portelli-cko Probably way too late, but what helped me was using lodash.isEqual to compare the previous value with the new value and only set the new value if there was a change. Because otherwise, since [] === [] is false, you are basically replacing the array every time, and I think that's resetting other stateful values under the hood. So, something like this should help touched persist:

// import { usePrevious } from 'react-use'
// this is assuming `files` is coming from props
const previousFiles = usePrevious(files) ?? []

// ...

// somewhere else within Formik render, probably in your `onChange`
if (!isEqual(files, previousFiles)) {
  formProps.setFieldValue('files', files)
  formProps.setFieldTouched('files', true, false)
}