react-final-form: Trigger validation on both onChange and onBlur

Feature request

What is the current behavior?

If true, validation will happen on blur. If false, validation will happen on change. Defaults to false.

What is the expected behavior?

I would suggest adding another config option; validateOnChange: bool validateOnBlur: bool

Other information

I’m considering abandoning my own attempt at a form library (it was a great learning experience though), since you certainly know more about the different pain points users have, and the internal workings of React and how to properly do performant libraries, considering your background with forms and React.

One thing I really liked with my solution was the ability to do sync validation onChange, and async (i.e network requests) on onBlur.

I haven’t figured out another way to do this just yet. But If we can have the library to validate on both onChange and onBlur this would be possible.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 6
  • Comments: 17 (4 by maintainers)

Most upvoted comments

I encountered a very similar issue today and I think I found an almost ideal solution.

At first, the documentation for Field validate caught my attention - it passes field meta as a 3rd argument, thus we can detect in our field validator if we validate on blur or on change – the meta.active is true during onChange validation and false during onBlur. Note, that Form validatate receives only the values parameter, thus is unable to detect which validation we are in, but I favor Field validation in all cases as it is more declarative, simpler, and easier to maintain.

But now, how to trigger validation on the change as well as on blur? We could simply leave the Form validateOnBlur property untouched, thus our validate function will run on change only, then write some of our validators to skip validation, when the field is in meta.active and then rerun the validation onBlur.

Well, it turns out, triggering the validation is another nut to crack! Unfortunately, I did not found a public API for triggering a validation, but I dig into the source code and found the mighty runValidation function, which is not exported but is triggered by change, which we can control, so I created a simple mutator which, when called, “changes” the value of the specified field, to the same, unchanged value – it fakes “change” to just trigger validation.

And, with all these little components, we are able to run some validations on change and some validation on blur.

Check the CodeSandbox and note the beautiful validator composition, which runs my async validator onBlur only when all synchronous validator passes, whilst synchronous validators are run on every change. https://codesandbox.io/s/trigger-validation-onblur-mx7vx

NOTE: the example is forked from another example and a little bit complicated than necessary, but the important part shall be clear. The fundamental parts are triggerValidation mutator and an async field-level validation function that tests meta.active.

A possibly related behaviour I quite like is having “onBlur” validation initially, and after the initial blur, validation is done onChange.

@erikras I think this issue is not actionable without your decisions regarding those newly proposed features. If you ask me, the idea seems to make sense. It’s important to figure out an intuitive API for it.

@Zn4rK Your example inspired me to create this example. It uses setFieldData to maintain a completely separate layer of errors that are validated on blur. What do you think? It seems like you could use both this “blur layer” and the built-in onChange layer to achieve your goals.

Edit 🏁 React Final Form - Custom Validation Engine

Thank you, I’ve actually used it before a few years back but forgot about it when I started writing this library. And I was about to wrap it up today too, looks like it’ll be a few more days 🤣

My 2c: I think validateOnBlur should not disable validateOnChange. Here is one example why. I’m triggering form submission on mouse down on a save button, and that doesn’t blur the currently active field, so the form is submitted even though the validation doesn’t pass.

Why do I use onMouseDown instead of just onClick? That’s because I display the errors from the validation under the inputs. When there are errors at submission, the save button is pushed down, sometimes way inder the current mouse pointer, and this cancels the click event.

A little update on this issue; I’ve been experimenting with this for a few hours today, and initially I thought it would work great. Don’t get me wrong, it does work great and I can probably still use it - but lifting parts or all validation responsibility from the library introduces another set of issues;

  • We have no way of knowing if the form is done validating in onSubmit() (edit: this was bad phrasing on my part, we can of course read the “validating”-props in onSubmit, and I guess we could subscribe to changes to “data” to verify that the validation is done). Just disabling the button and waiting for the form to validate it self is not good UX - we could build a promise based solution in OnBlurValidation, but that’s something that the library already does when using the “normal” validations.

  • If a user submits the form our extra layer of validation will not be run in the current example. We’ll have to build something that triggers all extra validation when the form is submitted. Try it - insert anything in the “lastName” field to make the form not be pristine, and then submit.

I only have loose thoughts on how we could go about resolving these issues (or the original one).

I’ve almost managed to hack up a solution using a <FormSpy /> to detect changes to active fields to trigger a validation using a mutator (which in turn does nothing, but mutators always triggers the validation), and then using a <Field /> to add a field-level validator the reads the current reactFinalForm.getState() via context. It’s ugly, and I was mostly curious of what it would take to make it work.

The only issue with this is that the mutators always trigger the validations so if I want to set some field data in the validate (loading indicator) I get a recursive loop. I could use reactFinalForm.pauseValidation() since I’m already using the context, but then I’m introducing a race condition - another field might not validate.

My thoughts I had while I was hacking this^ up was;

  • FieldState should have a validating property.
  • The validators should have access to final-form’s FormState
  • There should be a way to call runValidation() via FormRenderProps.
  • It would be awesome if field-level validators could be specified via FormProps directly

I’ll end this update in some pseduo code:

<Form
  render={(formRenderProps) => {
    <React.Fragment>
      {/* Other stuff */}
      <Field
        name={name}
        // It would probably be awesome to have FieldState here as well...
        validate={(value, values, FormState) => {
          // Normal sync validation
          if(!value) {
            return 'This field is required';
          }
  
          // Check if we can run async validation
          if(FormState.active === undefined) {
            return new Promise(resolve => {
              setTimeout(() => {
                if(value === 'admin') {
                  resolve('No way!');
                }
                
                resolve();
              }, 2000);
            })
          }
        }}
      />
      {/* Other stuff */}
      <FormSpy
        subscription={{ active: true }}
        onChange={({ active }) => {
          const { active: previouslyActiveField } = formRenderProps;
  
          if (active === undefined && previouslyActiveField !== undefined) {
            // A blur occured, run the validation for the previously active field
            formRenderProps.validate(previouslyActiveField);
          }
        }}
      />
    </React.Fragment>
  }}
/>

Hopefully my thoughts here makes sense. If they do, we can extract some of them to new issues…

I’m re-opening this to facilitate the discussion. Feel free to close it if we should take the discussion somewhere else 😃