jaredpalmer / formik

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

Validate only one field at a time #512

Open orenklein opened 6 years ago

orenklein commented 6 years ago

Bug, Feature, or Question?

Feature/Question

Current Behavior

Currently, validation is running asynchronously on the whole schema (assuming you use ValidationSchema of yup). It is reasonable to run the validation on submit, however, It is extremely cumbersome to run it on every field change/blur.

Especially if you have some backend validation (using yup.test). Assuming you have a couple of fields, field A, and field B - the later must go through backend validation. Every time you change field A, the validation runs both on A and B which will cause unnecessary backend calls.

I also tried using the new alpha version's handleChange('fieldName') but I still experience the same behavior.

Suggested Solutions

  1. Using yup schema, using yup.reach seems reasonable, even though I'm not sure how is its performance (https://github.com/jaredpalmer/formik/issues/35)

  2. Formik validate function - pass the event's field name. It will allow the developer to validate only one field at a time.

Environment

wmortume commented 4 years ago

I've found a solution. You should only display an error message when the text input field value changes. Luckily, formik has a method called getFieldMeta for that and when you pass the field name to it, it returns an object. The only thing that changes from that object is the value prop which you would do a check on.

Example:

errorMessage={
 formikProps.getFieldMeta("username").value
  ? formikProps.errors.username
   : ""
}
iam-yan commented 4 years ago

Any update or plan on this? Running into the same issue.

I think the current behavior is quite bad in UX. Would like to see the request behavior work as default, or at least give an option in <Formik/>

cigzigwon commented 4 years ago

I believe there is no solution for this. Only workarounds. I would run the latest version and disable validation so that fields only validate on submit. But I agree. Validations should probably default to validating only upon blur for that field. I think that validation runs on the entire schema for cases that involve cross field validation. Either way Formik will do the job. You just have to be dillegent in your own effort. I can help if you get stuck.

cigzigwon commented 4 years ago

What is the specific behavior you are looking for?

cigzigwon commented 4 years ago

@clu7ch3r Except I think you want to return undefined instead of an empty string because I beleive your field will be in error state with a blank message.

iam-yan commented 4 years ago

@cigzigwon I think even for cross field validation, the current behavior is not the best way from the ux perspective. I'd expect something maybe like validate group.

I am simply building the behavior that validating only occurs upon blur for that field.

The problems I am running into is if there was anyway to build a workarounds with <Field/> and <ErrorMessage/>, in other words I would not have to pass all the event handlers manually...

And another question would be how to use Yup for field level validation...

Please give me some clue if you can.

wmortume commented 4 years ago

@cigzigwon Mines work just fine. It only displays errors when there is one and the empty string is the placeholder space for when there is one.

wmortume commented 4 years ago

@juuyan Have you tried my solution? So far it works on TextInput and RNE Input component.

iam-yan commented 4 years ago

@clu7ch3r Hey thx I see your idea and will try it. But in your example you are not testing the change of the field's value, but its existence, aren't you?

wmortume commented 4 years ago

@juuyan I was checking the change in the field's value. formikProps.getFieldMeta("username").value checks for change in username field, not if it exist. It would probably throw an error or do nothing if I passed in a field name that doesn't exist.

iam-yan commented 4 years ago

@clu7ch3r Hmm but I have logged and getFieldMeta(field-name).value is the current value of field instead of the state of change. So I tried to store the value for comparing, yet have observed some side cases caused by repetitive state updating... Did I miss something?

iam-yan commented 4 years ago

Share my workarounds here in case it could be helpful to someone.

I use a ref to store if a field should be considered as validated, with which I determine if the error message should be visible or not.

const { errors, handleBlur, isSubmitting } = useFormikContext()
const validated = useRef(false)

let errorMsg
if (isSubmitting && !validated.current) validated.current = true
if (validated.current) errorMsg = errors[name] && errors[name]

And update it in onBlur and onFocus

<Field
   onBlur={e => {
      validated.current = true
      handleBlur(e)
   }}
   onFocus={() => (validated.current = false)}
/>

The benefit is you can still control the validate timing and schema with <Formik/> and Yup, but under the hood the validating will still execute on all the fields.

If you are looking for true single field validation, check this one: https://github.com/jaredpalmer/formik/issues/512#issuecomment-473040060

wmortume commented 4 years ago

@juuyan The value updates as you type so it would also be a state of change. I checked if the current value of the field has something in it and if it does, display the error for the specified field. With that being said, the error would only show up when typing in the specific field, not on all of them. Some previous comments above had already shown how to do it using Formik's Field component but my solution is one line of code and it's for those using the built-in TextInput component or third party ones like React Native Elements Input component.

terenceodonoghue commented 4 years ago

Is there a solution for this, or a plan to prevent blurring a single field from re-validating the entire form? I'm otherwise happy with Formik but this specific issue is infuriating.

cigzigwon commented 4 years ago

@terenceodonoghue Instead of getting so upset you might want to step back for a minute and re-consider your implementation. I may be able to help with that. How long have you been using Formik? I thought the same at first but now realize that @jaredpalmer has given us everything we need here to solve this. You should start by disabling validation altogether and leverage the Formik 'touched' prop as well as the Field validate prop to achieve what you are looking for. You can also just nix out Yup because we can use es6 to deal with validation of more complex data structuring that one may require. I'm not sure how complex you really need to be in working with your form. Is it a wizard? How many fields? I think the alternatives to Formik have other downfalls and that is why this project exists.

terenceodonoghue commented 4 years ago

My apologies @cigzigwon, I didn't mean to give the impression that I was upset.

My use case was validating the field value both synchronously (eg. 'value can't be empty') and asynchronously (validating value using a GraphQL hook).

If the async validation failed it would assign the error to asyncErrorStatus which could then be read by the field's validate function, but then blurring an unrelated field would re-trigger validation on the original field and flag it as okay because the value was no longer empty.

I wound up solving my problem with useEffect like so:

  useEffect(() => {
    validateForm();
  }, [asyncErrorStatus, validateForm]);

Basically, form validation behaviour proceeds as normal, but if the fetch error status changes, it triggers another validation and updates/clears field errors where appropriate.

Seems to work, but open to suggestions.

vsubbotskyy commented 4 years ago

To put my two cents in, I managed to validate fields separately by ditching validationSchema and using Formik's Field component and it's validate prop. And it works, but when I do:

await form.setFieldTouched(field.name, true, false);
await form.setFieldValue(field.name, value, false);

on input's onChange for instance, I get very annoying lag between keypress and letter appearing in the input. Not awaiting is not an option because I'd be validating against old values.

cigzigwon commented 4 years ago

@vsubbotskyy I also chose not to rely on Yup because it has pros/cons depending on the Formik implementation. The lag is a side-effect of your synchronicity. Since Formik relies on ephemeral state you are blocking IO by overriding onChange directly. What problem are you trying to solve w/onChange in this way? Both of those things happen by default on Formik's onChange handler.

cigzigwon commented 4 years ago

@terenceodonoghue Ahh. Your challenge is dealing with race conditions from back end validation onBlur? That adds another level of difficulty, understood. I think whatever works to get you unblocked but in retrospect. Nix out ur complexity and take note that when passing a validator to the withFormik hook that you get all of the Formik props. So you can implement a filter to validate only touched fields instead of everything. I would use an object map reducer function pipeline to achieve this so you can iterate through touched fields and reduce the errors into the object Formik wants.

cigzigwon commented 4 years ago
const errors = Object.entries(values).filter(([key, value]) => (
        // filter out untouched and whatever you want to skip
      ))
        .map(([key, value]) => {
         // run validation and push error object defs into a stack
        })
        .reduce((acc, error) => {
        // reduce the array stack into the error obect formik wants
      }, {})
return errors
ivanfuzuli commented 4 years ago

I use Field component with validation prop. but it also re-run all validations. Is there any work around? I make api calls and I don't wanna send request when other fields change.

I read a suggestion by @jaredpalmer that Field Level Validation will run only field change but it isn't solution. It re-run everytime. Do you have any example?

adtm commented 4 years ago

I today tackled also this issue and couldn't find a proper solution, but as current workaround came up just setting the fields to be touched on change. And showing the error only when they are touched. This prevents from checking all fields in one step and validates all of them on the submit. (It can be made to a simple wrapper even, but this gets the idea)

<TextField
    placeholder={i18n.t("FIRST_NAME")}
    value={values.firstName}
    error={showErrorIfTouched(touched.firstName, errors.firstName)}
    onChangeText={handleChange("firstName")}
    onChange={() => setFieldTouched("firstName", true)}
    containerStyle={styles.input}
/>
mateusvahl commented 4 years ago

Hey everyone, I'm running in the same issue:

onSubmit: Should continue work as it is, validating all your values against validationSchema.

handleChange handleBlur: Additionally, Inputs that have trigger handleChange and handleBlur will have their values tracked in another state. handleChange handleBlur Should trigger validation only on those specific fields.

cigzigwon commented 4 years ago

@mateuspv Try without Yup. Use field level-only or write something functional and pass that as your validator. You can use map/reduce functions which are more efficient than Yup. Also account for blocking IO with async calls if you need back-end validations.

mateusvahl commented 4 years ago

@cigzigwon Sorry I don't think it's about yup, is about be able of differentiate input validation from the entire form validation.

Even if i write my own validate function, is it running because it was trigger by handleChange or handleBlur ? Or it's because the user submitted the form(onSubmit)

validate: values => {
    const errors = {};

   // Did the user touched this? Is this a onSubmit or handleChange event ?
    if (!values.name) { 
      errors.name = 'Required';
    }

    // Same here
    if (!values.password) { 
      errors.password = 'Required';
    }

    return errors;
  }

Let me know if that makes sense for you, appreciate your reply

cigzigwon commented 4 years ago

@mateuspv Its not Yup. It still fires. But its okay to fire because its up to you to only display the errors when the field has been touched. Also theres nothing stopping you from checking touched internally. You can pass more than just values to validate. You can also use an external state to check via useState. Ultimately whats passed back in the errors strict is what controls display. Checks can go fast or slow depending on ur form field size. If the size is large then you should probably be building a wizard which will require a bit more dilligence to skip checks.

cigzigwon commented 4 years ago

@mateuspv To be specific,handleBlur behavior triggers setTouched on the field which then triggers the validation method OR Yup. handleSubmit would also do this validation. It all starts here: https://jaredpalmer.com/formik/docs/api/formik#validateonblur-boolean

Under the hood the process is as simple as outlined. Also in our forms we don't know if the user changed previous fields so I think that's why we just need to re-run validations. For your case you might want to look at withFormik so you can extend some of the behaviors outlined in the aforemention workflow. https://jaredpalmer.com/formik/docs/api/withFormik

That way you get the "Formik Bag" and you can override all of the form handlers in that process to run the shapes you need per touched Field. Scales out better in your Webpack builds too.

Does that help matey?

mateusvahl commented 4 years ago

@cigzigwon gotcha, I'm already using withFormik. I found my issue: I was manually calling validateForm instead of handleSubmit. This is why it was triggering all the validations.

Thanks for your help here! Appreciate.

tkodev commented 4 years ago

I've come to a similar solution to @adtm, while using material-ui and that is:

<TextField
  id="input-order-number"
  label="Order Number"
  type="orderNumber"
  name="orderNumber"
  onChange={handleChange}
  onBlur={handleBlur}
  value={values.orderNumber}
  error={Boolean(touched.orderNumber && errors.orderNumber)}
  helperText={
    (touched.orderNumber && errors.orderNumber) ||
    'Find it on your invoice'
  }
  fullWidth
/>

I'm using validationSchema so it's difficult to control the actual validation logic. However Yup is too useful to drop. It's make work if I'm using the same Yup schema on the backend and I'm rebuilding it for the front end slightly differently just to account for this.

tkodev commented 4 years ago

you can probably write a function that merges the touched and errors object in a way that each property contains the error message only if the touched value is true. Then you can avoid the whole touched.* && errors.*

alonspr commented 4 years ago

This works for me (using validations + custom fields):

<Formik
    validate={validateInputs} // normal for loop which return an error object
    validateOnChange={false} // turn off fields validation
    validateOnBlur={false} // turn off fields validation
    ...
/>

I build my own fields, so for every field's onChange and onBlur I add this logic: (validateInputs is the main validate function - you can pass it with context to components)

const validateSingleField = (name, value) {
    const error = validateInputs({ [name]: value });
    setFieldError(name, error[name]); // set / clear error message
};

const handleChange = e => {
    setFieldValue(name, e.target.value);

    // validate local field only
    validateSingleField(name, e.target.value);
};

const handleBlur = e => {
    setFieldTouched(name, true);

    // validate local field only
    validateSingleField(name, e.target.value);
};

<input onChange={handleChange} onBlur={handleBlur} {...} />

This way every change and blur validates the current field and not the entire form, this also keeps the default behavior for submit / reset normally. You can of course set more conditions to avoid reassignment of same values

cigzigwon commented 4 years ago

@alonspr This is exactly what I was refferring to!

560560 commented 4 years ago

Hi everyone! This is my solution:

Validation starts work only after first press submit button. So user can safely filling Fields, and he get error messages only after trying of submitting . <----------- Start component code --------------> import React, {useState} from 'react'; import {Button, Col, Container, Form, Row} from "react-bootstrap"; import {Formik} from "formik" import * as Yup from 'yup'

const getSDataFromForm = (formDadta) => { console.log(formDadta) } const Contacts = (props) => {

const [validationRequired, setValidationRequired] = useState(false)

let initialValues = {
    firstName: "",
    secondName: "",
    email: "",
    message: "",
    checkbox: false
}

const validationSchema = Yup.object({
    firstName: Yup.string().required("This field is required").max(10),
    secondName: Yup.string().required("This field is required").max(12),
    email: Yup.string().required("This field is required").email("Please enter valid email"),
    message: Yup.string().max(12),
})

return (

{({ handleSubmit, handleChange, isValid, values, touched, errors }) => (
First name Looks good! {errors.firstName} Second name Looks good! {errors.secondName} Email address We'll never share your email with anyone else. {errors.email} Message {errors.message}
)}
);

}

export default Contacts;

<----------- End component code -------------->

thenameisflic commented 4 years ago

Since I was already using yup, the easiest solution for me was caching the results of the check, like this:

  1. Create a function to wrap and cache the results of .test() calls

    const cacheTest = (asyncValidate) => {
    let _valid = false;
    let _value = '';
    
    return async (value) => {
    if (value !== _value) {
      const response = await asyncValidate(value);
      _value = value;
      _valid = response;
      return response;
    }
    return _valid;
    };
    };
  2. Write the test function as usual

    const checkEmailUnique = async () => {
    if (!isEmailValid(value))
      return false;
    const response = await validateUser({email: value});
    return response.data.ok;
    }
  3. Wrap our cached test inside a ref to prevent re-renders:

    const emailUniqueTest = useRef(cacheTest(checkEmailUnique));
  4. Just pass our ref to .test

    .test("email-unique", "This e-mail already has an account.", emailUniqueTest.current)

I really like this workaround, hopefully it can be useful for someone else.

loburets commented 4 years ago

I had a similar issue with validation of field value by API. It worked well, but user had to wait for API response even to see validation error for some other fields of the form.

To solve it I'm doing the async validation myself and put results to formik using setFieldError. But it also has few pitfalls, so maybe my example will be helpful for someone:

// component with the form:
const FormikExamplePage = (props) => {

  // use the apiError from component state as we need to have it stored outside of the formik
  // to not be reset after each validate cycle, but be reset only when we want
  const [apiErrorFromComponentState, setApiErrorToComponentState] = useState(null)

  return (
    <Formik
      validationSchema={yupSchema}
      validate={(values) => {
        const errors = {}

        // to not let formik reset it on validation
        if (apiErrorFromComponentState) {
          errors.firstNameApi = apiErrorFromComponentState
        }

        validateBirthdateBySeparateInputsAsTheSingleField(values, errors)

        return errors
      }}
    >
      {({
        errors,
        touched,
        isSubmitting,
        values,
        setFieldError,
        handleSubmit,
        setSubmitting,
        initialValues,
        setFieldTouched,
      }) => (
        <Form>
          Simple input + yup validation + api validation:<br/>
          <div>
            {/*
              * You can set the validate={validateFirstNameUsingApi} as props for formik.Field
              * It is the simplest solution which just works out of the box
              * But you maybe don't want to do it as it leads to delays during other validations
              * For example user edits the email field
              * Then the user see the error only when the api validation is done, despite yup rule doesn't need the api response to show the error
              * Even if the api validates the firstName, not the email, user still need to wait to see the email error
              * No solution currently is supported out of the box, see the https://github.com/formium/formik/issues/512
              * So, the component FirstNameFieldWithApiValidation is build to solve the issue
              * Also form submitting is overwritten to support it
              */}
            <FirstNameFieldWithApiValidation name="firstName" type="text" setApiError={setApiErrorToComponentState}/>
          </div>
          {/* Whatever logic of showing can be defined here, it is just example which looks reasonable for me */}
          { errors.firstName && touched.firstName ?
            <div style={{color: 'red'}}>{errors.firstName}</div>
              : errors.firstNameApi && touched.firstName ?
                  <div style={{color: 'red'}}>{errors.firstNameApi}</div>
                  : null
          }
            <Button
              type="submit"
              fullWidth
              color="primary"
              variant="raised"
              onClick={async e => {
                // overwritten formik submission just to add additional async validation before submitting
                e.preventDefault()

                if (isSubmitting) {
                  return
                }

                setSubmitting(true)
                // touch all fields to show the sync validation errors and don't force user to wait
                Object.keys(initialValues).forEach(key => setFieldTouched(key))

                const apiError = await validateFirstNameUsingApi(values.firstName)
                await setApiErrorToComponentState(apiError)
                setFieldError('firstNameApi', apiError)

                handleSubmit()
            }}>
              { isSubmitting ? 'Submitting...' : 'Submit' }
            </Button>
        </Form>
      )}
    </Formik>
  )
}

// example of field which is validated by api and yup both
const FirstNameFieldWithApiValidation = ({setApiError, ...props}) => {
  const {
    values: { firstName },
    errors: { firstName: firstNameError },
    setFieldError,
    setFieldValue,
    validateField,
  } = useFormikContext()

  const isMount = useIsMount()

  useEffect(() => {
    const doTheEffect = async () => {
      // update the field as touched but not on the first appearance
      // because we don't want to see the validation before user interacted with the field
      if (isMount) {
        return
      }

      // you can reset previous value if you want
      await setApiError(null)
      setFieldError('firstNameApi', null)
      // just to trigger rerender once again to reflect the error disappears till the new request will retur results
      setFieldValue('firstName', firstName)
      validateField('firstName')

      // some error is already here, so no need to validate on api
      // or maybe you want to do the api call and combine the errors, it's up to you
      if (firstNameError) {
        return
      }

      // you probably need some debounce mechanism here to not call api on each key down
      const apiError = await validateFirstNameUsingApi(firstName)
      setApiError(apiError)

      // the field is named as "firstNameApi" to understand difference if the field have error from api or not
      // it can help us to understand do we want to skip the api validation or not
      // otherwise we can not skip it based on the errors.firstName as it can be api error there which will not appear for the next api call
      setFieldError('firstNameApi', apiError)
    }
    doTheEffect()
  // set the value it only if something were changed, not the each render
  // it is also required to check if the firstNameError value was changed if you have the "if (firstNameError) { return }"
  // it works this way because value can be changed, but the error still not, so the validation would be skipped
  }, [firstName, firstNameError]);

  return (
    <Field {...props} />
  )
}

// just workaround to not run effect on the first render
const useIsMount = () => {
  const isMountRef = useRef(true);
  useEffect(() => {
    isMountRef.current = false;
  }, []);
  return isMountRef.current;
}
JulkaIII-zz commented 4 years ago

@thenameisflic You solution is the most convenient and reusable!

ElCapronWM commented 4 years ago

The issue here is in our algorithm and our understand of how validation schema works. I noticed that onChange the whole JSON object is validated and all functions are executed. So I added a simple decisional code:

carbon

I got around it without changing the way I implemented Formik. Here the back-end call only happens when the input is modified.

Note: If the input is not focused, any change of other input will execute all functions and our untouched will be assigned to false by default, so... if the input is changed -> validate and save the result to a global variable. So next time any other input is changed -> no validation required -> assign the latest value of validation.

cigzigwon commented 4 years ago

@caduceusGithub That's why I said Yup < Functional Pipeline (Custom)

Jared-Dahlke commented 4 years ago

yeah i just noticed it was validating all fields every keystroke when i throttled my CPU down 4x and put some console logs in my validation function.

this is my validationSchema , the topics array has over 2000 objects in it, definitely going to have to find yet another work around to get formik workin on this:

you can see here why validating every field wont work for me. So if i enter in a 5 character profileName, my topicsHasResponse function runs over 1000 times. I regret using formik for my work project. If i have to find a work around for everything then why am i even using a 3rd party tool :


    basicInfoIndustryVerticalId: Yup.number()
        .typeError('Required')
        .required('Required'),
    basicInfoProfileName: Yup.string()
        .min(2, 'Must be greater than 1 character')
        .max(50, 'Must be less than 50 characters')
        .required('Required'),
    basicInfoWebsiteUrl: Yup.string()
        .test(
            'urlTest',
            'Valid URL required (e.g. google.com)',
            (basicInfoWebsiteUrl) => {
                return urlRegex({ exact: true, strict: false }).test(
                    basicInfoWebsiteUrl
                )
            }
        )
        .required('Required'),

    basicInfoTwitterProfile: Yup.string()
        .min(2, 'Must be greater than 1 character')
        .max(50, 'Must be less than 30 characters')
        .required('Required'),
    topCompetitors: Yup.array()
        .typeError('Wrong type')
        .min(1, 'At least one competitor is required'),
    topics: Yup.array()
        .typeError('Wrong type')
        .test('topicsTest', 'You must include at least one topic', (topics) => {
            return topicsHasResponse(topics)
        }),
    scenarios: Yup.array()
        .typeError('Wrong type')
        .test(
            'scenariosTest',
            'Please select a response for each scenario',
            (scenarios) => {
                return scenariosAllHaveAResponse(scenarios)
            }
        ),
    categories: Yup.array()
        .typeError('Wrong type')
        .test(
            'categoriesTest',
            'Please take action on at least one category',
            (categories) => {
                return categoriesHasResponse(categories)
            }
        )
})

function categoriesHasResponse(categories) {
    console.log('running categoriesHasResponse')
    if (categories.length < 1) return false
    for (const category of categories) {
        if (category.contentCategoryResponseId !== 3) return true
    }
    return false
}

function topicsHasResponse(topics) {
    console.log('running topics has response')
    for (const topic of topics) {
        console.log('looping through topics')
        if (topic.topicResponseId == 1) return true
        if (topic.children && topic.children.length > 0) {
            const childHasResponse = topicsHasResponse(topic.children)
            if (childHasResponse) return childHasResponse
        }
    }
    return false
}

function scenariosAllHaveAResponse(scenarios) {
    console.log('running scenaios all have a response')
    if (scenarios.length < 1) return false
    for (const scenario of scenarios) {
        console.log('looping through scenarios')
        if (!scenario.scenarioResponseId || scenario.scenarioResponseId.length < 1)
            return false
    }
    return true
}```
johnrom commented 4 years ago

@JaredDahlke it's definitely understandable that this is a pretty severe limitation for some large datasets. the problem with solving it by validating only one field at a time, is that some fields might have dependencies causing the following issue:

// passed to fieldB.validate prop
fieldBisValid = (fieldA * fieldB) < 100;

// the validation here is correct in both systems
setValues({
  fieldA: 20,
  fieldB: 4,
});

// the validation run here currently works,
// but would not if we only validated fieldA.
setFieldValue('fieldA', 40, true);

The problem is how do we provide a way for one field-level validation to trigger another in a way that makes sense from both a developer perspective and a performance perspective. I think an API like the following makes some sense, but requires just gallons of other work to be done before implementing and without proper care could lead to an infinite loop of validations if both fields include each other.

// includes values.fieldA, touched.fieldA, errors.fieldA, 
// and triggers fieldA.validations when these validations are triggered
<Field name="fieldB" include={fields => { fieldA: fields.fieldA }} />

For now, you can move the very expensive validation to the Field level and it will only be validated when topics changes:

<Field name="topics" validate={validateTopicsHasResponse} />
itachiluan commented 3 years ago

Update: The code below doesn't really work. However: Setting the Formik validateOnBlur={true} validateOnChange={false} did the trick for me.

My particular need is just to make sure that the validator doesn't go to the API unnecessarily, so I've created a simple class that creates a state that caching the previous result, which, by it self, would also check email validity first (somehow through validationSchema, the validation of Yup.test() still fires up even the email value is not a valid email format):

const emailSchema = Yup.string().email().required();

export class UniqueEmailTester {

    static state() {
        return {
            lastState: {
                email: '',
                result: false
            }
        }
    }

    static async validator(email) {

        if (this.lastState.email === email)
            return this.lastState.result;

        this.lastState.email = email;
        try {
            await emailSchema.validate(email);
        } catch {
            this.lastState.result = false;
            return false;
        }

        try {
            const response = await Axios.request({
                baseURL: 'http://some.domain/api',
                url: '/user/uniqueemail',
                method: 'post',
                data: { email }
            });

            this.lastState.result = response.data.isValid;

            return response.data.isValid;
        } catch {
            this.lastState.result = false;
            return false;
        }

    }
}

then in the component I could just use it like this:

let emailValidatorState = UniqueEmailTester.state();

const validationSchema = Yup.object().shape({
        email: Yup.string().required("Email address is required.")
            .email("Invalid email address.")
            .lowercase()
            .test('checkEmailUnique', 'This address has already been registered with us', 
                UniqueEmailTester.validator.bind(emailValidatorState)),
        ...some_other_fields
})
jesseko commented 3 years ago

Changing this as proposed would also help a ton with server errors.

Jared wrote: "If you use setErrors, your errors will be wiped out by Formik's next validate or validationSchema call which can be triggered by the user typing (a change event) or blurring an input (a blur event)" (SO link)

But if that validation was only per-field then only the active field's error would get cleared, the rest of the field-level server errors would persist. Definite behavior improvement.


@johnrom would it be worth considering declaring the dependencies closer to the validation rather than directly on the Fields?

Not sure if Yup structures/bindings would allow for unpacking something like this, but just a thought:

validationSchema: Yup.object({
  isBig: boolean(),
  count: {  // optionally provide an object of this shape 
     dependencies: [ 'isBig' ],  // tell Formik to also validate this field when any of its dependencies change
     test: number().when('isBig', {   // the normal stuff you'd do with yup
        is: true,
        then: yup.number().min(5),
        otherwise: yup.number().min(0),
    }), ...
johnrom commented 3 years ago

@jesseko I don't know if Yup supports that, sorry (I don't use it).

The important determinations for making this change a reality are:

An Opt-In method

We need to provide a way to opt in to this new functionality to not break it for people using dependent fields in the current environment. We could either

We need a way to express validation dependencies, if we should support them. Some options:

Conclusion

When we determine paths for each of the above, we can implement something.

jesseko commented 3 years ago

FWIW I was suggesting that Yup structure with the idea that it'd be extra syntax on the Formik side, and Formik would process it to create 1) an internal structure representing those dependencies and 2) the Yup schema exactly as it looks today to actually execute validation.

Point taken about handling dependent fields as well. Looking at the dependent fields example, I see that MyField (textC) is getting values for fields it cares about (textA, textB) from useFormikContext.

Is the concern that with the optimizations in Formik V3, changes to textA and textB won't cause MyField (textC) to re-render without something like your include={} idea above to tell Formik about those dependencies?

johnrom commented 3 years ago

In v3, useFormikContext doesn't contain state, so the equivalent would be useFieldMeta(), but you cannot randomly add hooks depending on props, so that needs to itself be a prop like include that can define additional state to include within a single hook.

It's a little convoluted, but yes basically the same thing but very optimized.

ghost commented 2 years ago

Has this issue still yet to be resolved? 100's of comments on this issue dating back over 2 years as well, I must be missing something?

cigzigwon commented 2 years ago

@parker-upstart I've been using Formik for a while... Thought this WAS an issue but it's not. You need to understand how to disable automatic bindings and then do your own implementation. Yup seems to stand in the way. Use my own validator and it's less code and 10x faster. This issue should be closed...

ghost commented 2 years ago

@cigzigwon Yea, this is what I'm doing using a custom validate function - but with all the praise around how yup in tightly integrated I feel like this is still something which users would like to have. If that's not the direction the devs want to go that's fine, was mainly curious if I missed something in this thread.

nycgavin commented 2 years ago

I feel like if formik provided an implmentation for this, we don't have to roll our own implementation

kafeelkhatri commented 2 years ago

So onething what i found out is that using touched feature that will validate on handleSubmit and after that it will work perfectly by validating every field {({ handleChange, handleSubmit, errors, values, isValid, touched,})

Give it a try maybe it will help