Formik: form component renders twice + isValid turns true only on blur

Created on 30 Jan 2018  路  13Comments  路  Source: formium/formik

Hi everyone,

I have the following component using Formik ... i have two problems i am struggling to fix for half a day now :(

1) The console.log u see there renders twice per change ... so if i input 1 character my component renders twice
2) The form has only one input with basic validation ... for some reason ... while i type the email the form does become valid until the input looses focus

Can anyone give me any kind of feedback on what i am doing wrong?

thanks

const RecoverPasswordForm = ({ updateCaptchaHandle, processSubmit, history, values, errors, touched, isSubmitting, isValid, status, resetForm, setErrors }) => {
    console.log('render forgot password form');
    return (<div className="m-login__forget-password animated flipInX">
        <div className="m-login__head" style={{ marginBottom: '30px' }}>
            <h3 className="m-login__title">Forgotten Password ?</h3>
            <div className="m-login__desc">Enter your email to reset your password:</div>
        </div>
        <Form className={ "m-form m-form--fit m-form--label-align-right" + (errors.forgotPassword || (status && status.success) ? " m-t-20" : "") }>
            <ReCaptcha reference={updateCaptchaHandle} onChange={(captcha) => processSubmit(captcha, resetForm, setErrors)} />
            {status && status.success && <div className="m-alert m-alert--outline alert alert-success alert-dismissible" role="alert">
                <span>Please check you inbox! If we find any record of your email we will send there instructions for recover you password!</span>
            </div>}
            {errors.forgotPassword && <div className="m-alert m-alert--outline alert alert-danger alert-dismissible" role="alert">
                <span>We tried but something went wrong</span>
            </div>}
            <div className={"form-group m-form__group p-b-0" + (touched.email && errors.email ? " has-danger": "")}>
                <label htmlFor="email">Your Email</label>
                <Field className="form-control m-input m-input--air" type="text" id="email" name="email" autoComplete="off" placeholder="[email protected]" />
                <div id="email-error" className="form-control-feedback">{ touched.email && errors.email ? errors.email : '\u00A0' }</div>
            </div>
            <div className="row m-form__actions">
                <div className="col m--align-left">
                    {isSubmitting?"true":"false"} / {isValid?"true":"false"}
                    <button disabled={isSubmitting || !isValid} id="m_login_forget_password_submit" className={"btn btn-focus m-btn m-btn--pill m-btn--custom m-btn--air" + (isSubmitting ? " m-loader m-loader--light m-loader--left" : "")}>Request</button>
                </div>
                <div className="col m--align-right">
                    <button onClick={ () => !isSubmitting && history.push("/auth") } type="button" id="m_login_forget_password_cancel" className="btn btn-outline-focus m-btn m-btn--pill m-btn--custom">Cancel</button>
                </div>
            </div>
        </Form>
    </div>);
}


const RecoverPasswordFormHOC = compose(
    withRouter,
    graphql(ForgotPasswordMutation, {
        name: 'forgotPasswordMutation'
    }),
    withState('captchaHandle', 'setCaptchaHandle', null),
    withState('formValues', 'setFormValues', null),
    withHandlers({
        updateCaptchaHandle: ({ setCaptchaHandle }) => (handle) => setCaptchaHandle(handle),
        processSubmit: ({ formValues, forgotPasswordMutation, history }) => async (captcha, resetForm, setErrors) => {
            const { data: { forgotPassword: { status }} } = await forgotPasswordMutation({
                variables: {
                    email: formValues.email
                }
            });
            resetForm();
            if (status) {
                history.push({
                    pathname: '/auth',
                    state: { recoverEmailSent: true }
                });
            } else {
                setErrors({ forgotPassword: true });
            }
        }
    }),
    withFormik({
        mapPropsToValues({ email }) {
            return {
                email: email || ''
            }
        },
        validationSchema: Yup.object().shape({
            email: Yup.string().email(`That's not really an email`).required(`FYI we need your email to sign you in`).max(100, '100 characters we think are enough for your email'),
        }),
        validateOnChange: true,
        handleSubmit: (values, { props: { setFormValues, captchaHandle } }) => {
            setFormValues(values);
            captchaHandle.execute();
        }
    }),
    onlyUpdateForKeys(['isSubmitting', 'isValid', 'errors', 'touched', 'status', 'values']),
    pure
);

Most helpful comment

When we initially went with Yup, async was the _only_ option. It was pretty annoying tbh to design around. I think it'd be worth moving to sync Yup because I would take a guess that 95% of Yup schema's only use sync features anyways.

All 13 comments

@wowzaaa I have the exact same problem. I haven't yet found a solution. Which version are you using?

@wowzaaa I don't know if this helps, but I've shimmed Formik for now by wrapping it in another component that tweaks the validation logic. This has the effect of:

  • Validating the form on first mount (calculating isInitialValid based on the yup schema).
  • Validating the form as soon as a field is edited
  • Maintaining the existing per field "touched" logic.

I haven't focussed at all on finding out why it renders twice. My biggest issue is that I couldn't appropriately use isValid to control the UI state of my submit button - since a single field form won't trigger an isValid change until you navigate out of the field, which is a very confusing UX in our setup (you have to click the background to get the submit button to enable...)

The above changes assume that:

  • The component doesn't get re-initialised (I haven't added a componentWillReceiveProps handler)
  • The validation schema is synchronous
export class EnhancedFormik extends React.PureComponent{
  static propTypes = Formik.propTypes

  renderWithFixedValidation = (props) => {
    const {isValid,errors,dirty} = props
    const fixedIsValid = !dirty
      ? this.state.wasInitialValid
      : (isValid || Object.keys(errors).length === 0)

    return this.props.render(
      (fixedIsValid === isValid) ? props : {
        ...props,
        isValid:fixedIsValid
      }
    )
  }

  testIsInitialValid = () => {
    const {initialValues,validationSchema,isInitialValid} = this.props
    return isInitialValid === undefined
      ? validationSchema
        ? validationSchema.isValidSync(initialValues)
        : undefined
      : isInitialValid
  }

  constructor(props){
    super(props)
    this.state = {
      wasInitialValid: this.testIsInitialValid()
    }
  }

  render(){
    const {wasInitialValid} = this.state
    const preValidation = {isInitialValid:wasInitialValid}
    return <Formik
      validateOnChange={true}
      {...this.props}
      {...preValidation}
      render={this.renderWithFixedValidation}
      />
  }
}

To use it, I've simply replaced the use of Formik in my components with EnhancedFormik. Hope this helps.

Ok, a little update - the reason there are double renders is because Formik runs asynchronous validation - so there's one setState call which re-renders the form on every change, and another setState call which re-renders the form once the result of the async validation has been completed.

I thought I spied a hack to get around this by memoizing successive validation results but... there's a snag - it only works if you validate in a synchronous fashion. As soon as you drop to async, Formik's setState call will trigger a re-render regardless, because Formik is not a PureComponent / does not have special shouldComponentUpdate logic.

well for the last reasons i tried using recompose's onlyUpdateForKeys method .. little luck though ... but i will keep trying :)

@benvan so i decided to give up on formik ... switching to https://github.com/final-form/react-final-form

Sorry to hear that. IsValid and dirty was fixed in 11 which just came out. The double render is due to the fact that validation may or may not be async. Why double render? Think about the case of long running validation, you would not see the change until it was done.

look here ... no async no nothing

https://codesandbox.io/s/nrox23161l

Yup is async.

Yup has a sync method now. But if we moved to it. It would break everyone鈥檚 forms.

Edit: it would break lots of forms.

Here is the Yup validate sync docs: https://github.com/jquense/yup/blob/master/README.md#mixedvalidatesyncvalue-any-options-object-any

It throws if there is an async test

@jaredpalmer Makes sense.

For us, we've gone with the contract that we don't perform asynchronous validation on the inputs (the only exception is when the form gets submitted - technically the response may contain an error which applies to a field in the form - but this is not reflected in a yup schema)

This has allowed us to wrap Formik with a component that replaces validationSchema with its own custom validate method (which synchronously evaluates the schema)

In turn this allows us to put an onChange handler onto the form, which has an up-to-date isValid / errors / touched bag.

I appreciate it's very difficult to design a library for everybody's needs, and you have my thanks for the enormous effort already put into designing this one.

When we initially went with Yup, async was the _only_ option. It was pretty annoying tbh to design around. I think it'd be worth moving to sync Yup because I would take a guess that 95% of Yup schema's only use sync features anyways.

Was this page helpful?
0 / 5 - 0 ratings