react-hook-form: Validation not working when using onBlur mode
Asked Answered
W

2

33

I am trying to display an error with yup and react-hook-form when the user selects more than 5 checkboxes without success.

Instead, the error is shown when the seventh checkbox is selected.

Here is the simplified code:

imports...

const schema = yup.object().shape({
  option: yup.array().max(5)
});
function App() {
  const { register, handleSubmit, errors } = useForm({
    mode: "onBlur",
    resolver: yupResolver(schema)
  });

  const [state, setState] = useState({
    wasSubmitted: false
  });

  const submit = async (data) => {
    window.alert(JSON.stringify(data));
  };

  if (state.wasSubmitted) {
    return <p>Congrats</p>;
  } else {
    return (
      <>
        <CssBaseline />
        <Container maxWidth="sm">
          <Typography variant="h2" component="h1">
            My form
          </Typography>
          <form noValidate autoComplete="off" onSubmit={handleSubmit(submit)}>
            <FormControl
              component="fieldset"
              error={!!errors.option}
            >
              <FormLabel component="legend">
                Please select the category or categories of books the child is
                interested in:
              </FormLabel>
              <FormGroup>
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option1"
                  label="Option 1"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option2"
                  label="Option 2"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  label="Option3"
                  value="Option 3"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option4"
                  label="Option 4"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option5"
                  label="Option 5"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option6"
                  label="Option 6"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option7"
                  label="Option 7"
                />
                <FormControlLabel
                  control={<Checkbox name="option" inputRef={register} />}
                  value="Option8"
                  label="Option 8"
                />
                <FormControlLabel
              <FormHelperText>Up to five categories</FormHelperText>
            </FormControl>

            <Button
              type="submit"
              disableElevation
            >
              Submit
            </Button>
          </form>
        </Container>
      </>
    );
  }
}

export default App;

You can also find the project's sandbox here:

Edit modern-resonance-d7mpc

Any ideas?

Wellappointed answered 23/3, 2021 at 7:25 Comment(2)
Change mode of useForm to onChange: useForm({ mode: "onChange", ...Delegacy
Thank you! that fixed the issue. Also, I just realized that, if you change mode of useForm to useForm({ mode: "all"..., you get both behaviours onChanage and onBlurWellappointed
M
44

As @aadlc said, the solution is to set the mode to onChange or all. I'll explain the reason why.

From react-hook-form API docs:

mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'

Name Type Description
onSubmit (Default) string Validation will trigger on the submit event and invalid inputs will attach onChange event listeners to re-validate them.
onBlur string Validation will trigger on the blur event.
onChange string Validation will trigger on the change event with each input, and lead to multiple re-renders. Warning: this often comes with a significant impact on performance.
onTouched string Validation will trigger on the first blur event. After that, it will trigger on every change event.
all string Validation will trigger on the blur and change events.

In your code, the form mode is onBlur. it means the validation is triggered on blur event (unfocus the input). When you select the option n+1, it triggers the blur event from the option n.

e.g. Just before you select the 6th option (invalid), the blur event fires from the 5th option (valid) because you're no longer focusing it, and validate from option 1-5, so you have to check the 7th option to revalidate the option from 1 to 6.

-- select up to 5 options --
select option 4

blur event fires from option 4 -> validate -> pass
select option 5

blur event fires from option 5 -> validate -> pass
select option 6

blur event fires from option 6 -> validate -> fail
select option 7

Changing the valiation mode to onChange will validate after change event is triggered, when all of the values are up-to-date:

-- select up to 5 options --
select option 4

blur event fires from option 4
select option 5
change event fires from option 5 -> validate -> pass

blur event fires from option 5
select option 6
change event fires from option 6 -> validate -> fail

blur event fires from option 6
select option 7
change event fires from option 7 -> validate -> fail

Changing the valiation mode to all will validate both in blur and change events, which may be overkill in this workflow, but it also works.

Mccalla answered 23/3, 2021 at 12:11 Comment(2)
example: useForm({mode: 'onBlur'})Coryza
Saved my day! Took a couple hours looking for this configuration, appreciate it. :)Ashtoreth
D
10

If you're using controlled components onBlur might not be automatically firing. You will have to wire it manually:

useController docs
wrapper docs

export const MyControlledField = ({ fieldName }: { fieldName: string }) => {
  const methods = useFormContext();
  const value = useWatch({ name: fieldName });
  const controller = useController({
    name: fieldName,
    control: methods.control,
  });
  return (
    <TextField
      value={value ?? ''}
      onChange={(e) => {
        methods.setValue(fieldName, e.target.value);
      }}
      onBlur={controller.field.onBlur}
    />
  );
};

Or using a wrapped component:

export const MyWrappedField = ({ fieldName }: { fieldName: string }) => {
  const methods = useFormContext();

  return (
    <Controller
      control={methods.control}
      name={fieldName}
      render={({ field: { onChange, onBlur, value, name, ref }, fieldState, formState }) => (
        <TextField
          onBlur={onBlur}
          onChange={onChange}
          value={value}
          inputRef={ref}
        />
      )}
    />
  );
};
Dominik answered 5/1, 2023 at 20:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.