Formik: [Next] Should form automatically reset if passed-in props change?

Created on 16 Aug 2017  路  12Comments  路  Source: formium/formik

The scenario I'm trying to implement is the following:

  1. User clicks "edit"
  2. GraphQL query gets dispatched, form gets rendered with loading indicator
  3. GraphQL response gets returned, apollo updates underlying bound components
  4. Form gets filled with response data, loading indicator gets removed

The problem I'm running into is that mapPropsToValues() only gets called in the constructor and not in componentWillReceiveProps(), and as a result Formik never updates the form with the received values and just renders the empty state. Is there any way around this right now, or is this not supported?

With a slightly hacky workaround I'm currently passing down the values as a different prop so they reach the form component itself, where I'm doing a this.props.resetForm() whenever the prop changes. Does the job for now, but wondering if we could just use componentWillReceiveProps() in Formik itself. I realize it can be a bit tricky but by checking if the previous values prop were falsy and/or the values state is not dirty this should be rather safe I think.

Question done

Most helpful comment

If you use the <Formik> component you should be able to pass enableReinitialize, which will reinitialize when it detects your initialValues have changed. So what should work is something like this:

const emptyFormState = {
  id: undefined,
  firstName: '',
  lastName: ''
};

export default class MyForm extends React.Component {
  getInitialValues() {
    return this.props.data.user || emptyFormState;
  }

  render() {
    return (
      <Formik
        enableReinitialize
        initialValues={this.getInitialValues()}
        onSubmit={this.props.onSubmit}
      >
        <YourFormLogic />
      </Formik>
    )
  }
}

MyForm.propTypes = {
  data: PropTypes.shape({
    user: PropTypes.shape({
      id: PropTypes.number.isRequired,
      firstName: PropTypes.string.isRequired,
      lastName: PropTypes.string.isRequired
    })
  }).isRequired,
  onSubmit: PropTypes.func.isRequired
};

Your user query data starts off undefined -> apollo fires the request -> apollo receives the response -> user prop gets updated -> MyForm re-renders -> Formik detects change in initialValues -> Formik reinitializes the form.

That said, you could also very easily block rendering of <Formik ... /> and just return null or a <Loader /> as long is this.props.data.user === undefined. Depends what behavior you want.

All 12 comments

Yeah that's the suggested solution at the moment as described here: https://github.com/jaredpalmer/formik#example-resetting-a-form-when-props-change

You do raise a solid point though...should Formik do this automatically.

Oh jesus, I missed that example completely :).

Implementing componentWillReceiveProps(newProps) and calling mapPropsToValues(newProps) will fix this, however there's a risk the form might already be dirty, in which case there might be a few scenarios playing out:

  • The prop change was incidental, due to a re-render being triggered in the parent context. You don't really want to recompute values because it's most likely going to reset your existing input.
  • The prop change was done on purpose, e.g. an edit form receiving the data or an input handler triggering an async load of additional form fields or something like that, people can be very creative.

I think the edit-data scenario is a very common one. Might be because I'm using it myself but in conjunction with e.g. apollo this is one of the first issues you encounter, unless you deliberately delay form rendering somewhere in a parent until the data comes in :).

One possible direction could be implementing (optional) awareness of some kind of initialization phase, e.g.

const withFormik = Formik({ isInitialized: (props) => !props.isLoading })

used as

<FormWithFormik isLoading={this.props.data.isLoading} />

And in Formik something like this:

componentWillReceiveProps(newProps) {
  if (!isInitialized(this.props) && isInitialized(newProps)) {
    this.setState({
      values: mapPropsToValues(newProps)
    });
  }
}

This does not really cover lazy loading of additional form data though, I think that can only be covered with something like mergePropsToValues(props, nextProps, currentValues).

edit: Looking at the two, I'm starting to favour the mergePropsToValues(props, nextProps, currentValues) approach as it's not limited to just initialization and can be used for lots of dynamic things. With a shallow compare this should also have very little impact in case it gets hit a lot.

const mapProps = (values) => values;

const withFormik = Formik({
  mapPropsToValues: (props) => mapProps(props.values),
  mergePropsToValues: (props, newProps, currentValues) =>
    (props.isLoading && !newProps.isLoading)
      ? mapProps(props.values)
      : currentValues
});
componentWillReceiveProps(newProps) {
  if (typeof mergePropsToValues === 'function') {
    const values = mergePropsToValues(this.props, newProps, this.state.values);

    if (values !== this.state.values) {
      this.setState({ values });
    }
  }
}

I've just come across this whilst using apollo,
Something like mergePropsToValues feels like the way to go, it should be safe with the fields to never become dirty if you do not render the form whilst props.data.loading / props.data.error is set

@pleunv have you started a PR for this?

For now I'm going to render my edit forms on a condition that data.loading and data.error are falsy.

I've written a HigherOrderComponent which renders my form when the data in apollo is ready.


const EditForm = ({mapPropsToValues}) => Form => props => {
    if (props.data.loading) {
        return <CircularProgress />
    }
    if (props.data.error) {
        return <ErrorLoading />
    }

    return <Form {...props} values={mapPropsToValues(props)}/>
}

export default compose(
     Formik({
    validationSchema: Yup.object().shape({
        firstname: Yup.string().required('Required'),
        lastname: Yup.string().required('Required'),
        email: Yup.string().email().required('Required'),
    }),
    async handleSubmit(values, { props, setSubmitting, setErrors }) {
             ....
        }
    }),
    EditForm({mapPropsToValues: props => {
    return {
        firstname: props.data.Contact.firstname,
        lastname: props.data.Contact.lastname,
        email: props.data.Contact.email,
        tags: props.data.Contact.tags
    };
   })
)(MyEditForm);


Can someone point me in the right direction? I'm using Apollo and trying to use Formik via render props, so I'm not sure how to interpret the above code samples. The example linked seems to have been removed, too.

In my case, my server-side-render (next.js) is completing with undefined for my data and then when we get down into the browser the data fetch happens, successfully, but my form just sits there based on undefined and not updating.

If you use the <Formik> component you should be able to pass enableReinitialize, which will reinitialize when it detects your initialValues have changed. So what should work is something like this:

const emptyFormState = {
  id: undefined,
  firstName: '',
  lastName: ''
};

export default class MyForm extends React.Component {
  getInitialValues() {
    return this.props.data.user || emptyFormState;
  }

  render() {
    return (
      <Formik
        enableReinitialize
        initialValues={this.getInitialValues()}
        onSubmit={this.props.onSubmit}
      >
        <YourFormLogic />
      </Formik>
    )
  }
}

MyForm.propTypes = {
  data: PropTypes.shape({
    user: PropTypes.shape({
      id: PropTypes.number.isRequired,
      firstName: PropTypes.string.isRequired,
      lastName: PropTypes.string.isRequired
    })
  }).isRequired,
  onSubmit: PropTypes.func.isRequired
};

Your user query data starts off undefined -> apollo fires the request -> apollo receives the response -> user prop gets updated -> MyForm re-renders -> Formik detects change in initialValues -> Formik reinitializes the form.

That said, you could also very easily block rendering of <Formik ... /> and just return null or a <Loader /> as long is this.props.data.user === undefined. Depends what behavior you want.

@pleunv thanks for the reply. Adding enableReinitialize does seem to fix that problem, but now no matter what I do I can't seem to get rid of the uncontrolled-to-controlled warning. I tried stripping down my form to contain nothing but the simplest possible implementation of one field and only that one field:

<input
    type="text"
    name="userId"
    value={values.userId}
    onChange={handleChange}
    onBlur={handleBlur}
/>

Even this causes the uncontrolled-to-controlled React error.

Console logging the data from within my formik render prop returns:

{
    firstName: "Adam",
    lastName: "Tuttle",
    userId: "4"
}

And still I get the error... I don't see anything in the docs about having to change anything about primitives (or anything else) when using enableReinitialize.

Here's a gist of the entire UserForm component: https://gist.github.com/atuttle/94db42c511b9d4cdcb642cfa116cd928

Looking at your gist that's normal, since your values will be undefined until apollo gets a response back from your query. 2 options:

  • You initialize your values with some kind of default value (i.e. empty string - see my emptyFormState example) since input components will throw that uncontrolled-to-controlled warning when you initialize them with undefined.
  • You block rendering until this.props.data.user !== undefined or this.props.loading === false.

Aha! Thank you so much. It makes perfect sense now.

Any updates on this. We have about few autocomplete fields in form and each time they update all the input fields get cleared.

componentWillRecieveProps is deprecated and any way to use getDerivedStateFromProps

Was this page helpful?
0 / 5 - 0 ratings