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?
What happens when you call setState() inside render() method? Nothing happens.
In constructor , we should avoid using setState() because this is the only place we directly assign the initial state to this. state . Also, we cannot directly put it in render() either since changing state each time triggers re-rendering which calls setState() again. This will result in an infinite loop.
Yes. It calls the render() method every time we call setState only except when shouldComponentUpdate returns false .
The first thing React will do when setState is called is merged the object you passed into setState into the current state of the component. This will kick off a process called reconciliation. The end goal of reconciliation is to, in the most efficient way possible, update the UI based on this new state.
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
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With