-
-
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 |