Skip to content

validate and validationSchema get the old values instead of the new values (when triggered via validateForm or validateField) #4035

@lukaszmakuch

Description

@lukaszmakuch

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions