8

I have the following component that takes a render prop that it passes values to a child component. Here is a codesandbox that shows the problem. Press submit and look at the console.

Here is the component:

export const FormContainer = function FormContainer<V>({
  initialValues,
  validate,
  render,
  ...rest
}: FormContainerProps<V>) {
  const [hasValidationError, setHasValidationError] = useState(false);
  const dispatch = useDispatch();

  useEffect(() => {
    if (!hasValidationError) {
      return;
    }

    scrollToValidationError();

    () => setHasValidationError(false);
  }, [hasValidationError]);

  return (
    <>
      <Formik
      >
        {({
          isSubmitting,
          submitCount,
          isValid,
          errors,
          values,
        }: FormikProps<V>) => {
          const invalid = !isValid;
          const submitted = submitCount > 0;

          if (submitCount > 0 && invalid) {
            setHasValidationError(true);
          }

          return (
            <>
              <Form>
                  <div className={styles.form}>
                    {render({
                      values,
                      errors,
                      isSubmitting,
                      invalid,
                      submitCount,
                    })}
                  </div>
              </Form>
            </>
          );
        }}
      </Formik>
    </>
  );
};

If there is a validation error then setHasValidationError is called which causes this error from react

Warning: Cannot update a component (`FormContainer`) while rendering a different component (`Formik`). To locate the bad setState() call inside `Formik`, follow the stack trace as described in 
    in Formik (created by FormContainer)
    in FormContainer (created by Home)
    in Home (created by Context.Consumer)
    in Route (created by App)
    in Switch (created by App)
    in Router (created by App)
    in App

I'm not saying this warning is wrong. Calling setHasValidationError does not seem ideal here but the call to scrollToValidationError(); that will get called in the initial useEffect hook is async and it needs to go outside the render function.

What can I do?

dagda1
  • 26,856
  • 59
  • 237
  • 450

2 Answers2

9

In order to avoid this issue with Formik, you can wrap your state calls in setTimeouts, this should do the deal:

        setTimeout(() => setHasValidationError(true), 0);

This is also what Formik does in their official documentation. It's a problem they have had for a while, the trick is to make the state update run on the next cycle tick.

Also see: https://github.com/jaredpalmer/formik/issues/1218

Ali Nasserzadeh
  • 1,365
  • 7
  • 15
  • I think `useEffect` instead of `setTimeout` is even better, so you can avoid the unfortunate case when `setState` is called after the component got unmounted. I did a safety check into the [source code](https://github.com/jaredpalmer/formik/blob/26c4f8627a5ecfd81ec2196c7a9687b3f39f2836/packages/formik/src/Formik.tsx#L1024), it looks fine, though eslint naively complained. – hackape Apr 07 '20 at 03:34
2

I think Ali's answer using setTimeout is legit. I'd like to add that useEffect is IMO better solution. For it further prevent the unlikely but still possible error case when setHasValidationError is called after the component got unmounted.

(eslint naively complains useEffect is unsafe to use here but I checked with source code it's totally fine.)

// here I rename useEffect to mute eslint error
const nextTick = useEffect;

<Formik>
  {({
    isSubmitting,
    submitCount,
    isValid,
    errors,
    values
  }: FormikProps<V>) => {
    const invalid = !isValid;

    nextTick(() => {
      if (submitCount > 0 && invalid) {
        setHasValidationError(true);
      }
    }, [submitCount, invalid]);

    // ...
  }
</Formik>
hackape
  • 18,643
  • 2
  • 29
  • 57
  • 1
    Reminder: The [React docs suggest](https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level) we call `useEffect` in the same order every time in a component. This means `useEffect` should come before any conditional returns, not run in loops, and not run in callbacks. The Formik example is using the `useEffect` in the callback for ``. For this reason, the `setTimeout()` approach makes the most sense. – Mike Mathew Apr 15 '20 at 12:54
  • You're right. I did take that into consideration, thus I checked the source code. For current implementation it's fine. But yeah, no guarantee that Formik will keep consistent internal implementation so your reasoning holds. – hackape Apr 16 '20 at 02:40