-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Bug report
Current Behavior
In Formik, the errors object reflects all the validation errors that were the result of running the validate function.
That's something I relied on when developing my app. It all worked fine, until one day I added a seemingly unrelated component and validation broke.
I embarked on a journey to find the cause of the bug. I started by stripping down the app to the bare minimum that reproduced the bug. As I was doing that, I discovered that the bug disappeared when I removed a console.log statement! It left me flabbergasted! 😨
Here's code that works as expected:
import { Form, Formik, useFormikContext } from "formik";
import { useEffect } from "react";
type FormValues = {
myField: "a" | "b";
};
const initialValues: FormValues = {
myField: "a",
};
const validate = (values: FormValues) => {
return { myField: `validated ${values.myField}` };
};
export default function BugReproduced() {
return (
<Formik<FormValues>
initialValues={initialValues}
onSubmit={() => {}}
validate={validate}
>
{({ errors, values }) => (
<Form>
values:<pre>{JSON.stringify(values, null, 2)}</pre>
errors:<pre>{JSON.stringify(errors, null, 2)}</pre>
<Observer />
</Form>
)}
</Formik>
);
}
function Observer() {
const {
values: { myField },
setFieldValue,
validateForm,
} = useFormikContext<FormValues>();
useEffect(() => {
(async () => {
if (myField === "a") {
await setFieldValue("myField", "b", false);
await validateForm();
}
})();
}, [myField, validateForm, setFieldValue]);
return null;
}Running it renders:
values:
{
"myField": "b"
}
errors:
{
"myField": "validated b"
}
Now, if we add a loop with console.log statements, like this:
import { Form, Formik, useFormikContext } from "formik";
import { useEffect } from "react";
type FormValues = {
myField: "a" | "b";
};
const initialValues: FormValues = {
myField: "a",
};
const validate = (values: FormValues) => {
return { myField: `validated ${values.myField}` };
};
export default function BugReproduced() {
return (
<Formik<FormValues>
initialValues={initialValues}
onSubmit={() => {}}
validate={validate}
>
{({ errors, values }) => (
<Form>
values:<pre>{JSON.stringify(values, null, 2)}</pre>
errors:<pre>{JSON.stringify(errors, null, 2)}</pre>
<Observer />
</Form>
)}
</Formik>
);
}
function Observer() {
const {
values: { myField },
setFieldValue,
validateForm,
} = useFormikContext<FormValues>();
useEffect(() => {
(async () => {
if (myField === "a") {
for (let i = 0; i < 500; i++) console.log(i); // <-- ADDED LINE
await setFieldValue("myField", "b", false);
await validateForm();
}
})();
}, [myField, validateForm, setFieldValue]);
return null;
}it renders:
values:
{
"myField": "b"
}
errors:
{
"myField": "validated a"
}
And that's surprising, because it means that errors is eventually set to what validate returned for values: { myField: "a" } and not for values: { myField: "b" }.
There's an inconsistency between values and errors.
What's especially tricky is that it appears to surface only when we add something seemingly unrelated to a previously perfectly functioning form, such as another component, some costly loop, or an unrelated async someFn().
From the little debugging I did, it seems that if that loop (or anything that's costly) is present, then validate is called first with the new values, and then with the old values.
Expected behavior
I understand that errors may not always be updated, especially when we pass false to calls like setFieldValue("myField", "b", false).
However, I'd expect that if there are calls to validate, then it's the most recent result that gets assigned to errors.
Reproducible example
https://codesandbox.io/p/devbox/formik-state-and-errors-forked-rh69jf
Additional context
Your environment
| Software | Version(s) |
|---|---|
| Formik | 2.6.6 |
| React | 18.2.0 |
| TypeScript | 4.4.4 |
| Browser | Chrome 137.0.7151.120 |
| npm/Yarn | latest |
| Operating System | Mac |