2

Taking a look at this commit in the Formik.tsx file, the validation was moved from (in the case of touched as an example):

 React.useEffect(() => {
    if (
      prevState.touched !== state.touched &&
      !!validateOnBlur &&
      !state.isSubmitting &&
      isMounted.current != null
    ) {
      const [validate, cancel] = makeCancelable(validateForm());
      validate.then(x => x).catch(x => x); // catch the rejection silently
      return cancel;
    }
    return;
  }, [
    prevState.touched,
    state.isSubmitting,
    state.touched,
    validateForm,
    validateOnBlur,
    isMounted,
  ]);

to here:

 const setFieldTouched = React.useCallback(
    (
      field: string,
      touched: boolean = true,
      shouldValidate: boolean = true
    ) => {
      dispatch({
        type: 'SET_FIELD_TOUCHED',
        payload: {
          field,
          value: touched,
        },
      });
      return validateOnBlur && shouldValidate
        ? validateFormWithLowPriority(state.values)
        : Promise.resolve();
    },
    [validateFormWithLowPriority, state.values, validateOnBlur]
  );

Jumping to the current implementation, it looks like this:

  const setFieldTouched = useEventCallback(
    (field: string, touched: boolean = true, shouldValidate?: boolean) => {
      dispatch({
        type: 'SET_FIELD_TOUCHED',
        payload: {
          field,
          value: touched,
        },
      });
      const willValidate =
        shouldValidate === undefined ? validateOnBlur : shouldValidate;
      return willValidate
        ? validateFormWithHighPriority(state.values)
        : Promise.resolve();
    }
  );

But why and how do the latter two ensure that the validation will be run with the "freshest" values? With the effect you might dispatch a change, then a blur (both will change the state). The state will then change at some point causing the effect to then run with the current values.

In the latter, the validation - when following the trail of calls - should still run synchronously (executor function of a promise is executed synchronously) - only the setting of the errors will wait until the promise is done (has explicit then). This should mean that when i call setFieldValue and then setFieldTouched immediately after, that the state will still be the old one (since state change is asynchronous). An example would be that i could dispatch a change to a state, and try to access the same state immediately after, which will still be the old state (code sandbox).

However when looking at this code sandbox, the values passed to validate (from the validation flow in setFieldTouched are the actual fresh values. I don't see why this is the case...this might very well be a mistaken interpretation of the flow on my side, but that is why i pose this question.

Dean
  • 521
  • 3
  • 14
  • I would be really happy about **any** input/pointers :) – Dean Jul 15 '22 at 18:49
  • It looks like you are using redux or at least `useReducer`. But you've only shown the events you are dispatching, not how the actions are implemented in the reducer, so we can't tell what's going on. – Bergi Jul 17 '22 at 09:59
  • you mean in the code examples? those are from formik, not me..im just trying to understand why the described behaviour is happening, even though in my understanding it shouldn't. I did not want to paste their entire Formik.tsx file in there, which is why i linked the current file i am inquiring about in the **"it looks like this"** in my question. If you want i can make it explicit and paste all of the code from the reducer and its util helper in here. but i think it blows up the question in terms of length – Dean Jul 17 '22 at 10:04
  • i realize that to understand the situation, one needs to delve a little into the linked source code, which is probably a lot to ask for only 50 + 25 reputation. But i'm hoping someone will do so anyway and share their conclusions, in order to facilitate some learning. This question really bugs me :p – Dean Jul 17 '22 at 15:41

1 Answers1

3

Edit

Turns out, the whole question stemmed from codesandbox using an old version of formik@1.3.2, which has different behavior from the current version of Formik. This led to confusion about the expected behavior from reading the source of formik@2.1.6 but seeing behavior that did not match the source.

It's a classic pitfall, reading or editing one source file, but executing another.

Original answer below:


You are asking a bunch of very interesting questions. I'll try to answer all of them, but first I want to talk a bit about how React rendering works in general, as that will inform the answers below.

First, let's look at this code example:

export default function App() {
  const [count, setCount] = useState(0);
  const doubleCount = useMemo(() => count * 2, [count]);
  const buttonRef = useRef();

  return (
    <div className="App">
      <button
        ref={buttonRef}
        onClick={() => {
          setCount(count + 1);
          console.log(count, doubleCount, buttonRef.current.textContent);
        }}
      >
        {doubleCount}
      </button>
    </div>
  );
}

(based on: https://twitter.com/RyanCarniato/status/1353801009844240389/photo/1)

Code Sandbox

Clicking the button 3 times prints:

0 0 "0"
1 2 "2"
2 4 "4"

This illustrates an important point - at the point where the click event is executed and you call setState(count + 1), the setState() operation itself has not been executed - it is instead scheduled as a change that will be resolved by the next React render. The value of count variable has not immediately increased. This is because the onClick() function was created when the last render happened, and it has captured whatever value count had at that time.

You must treat variables that come from React hooks like useState(), useReducer() and useMemo() as immutable - they do not change inline, they will only be updated with the next render.

This means that calling setState() itself a few times might also not do exactly what you expect. Let's say we start with count = 0.

setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
console.log(count) // 0, it will be 1 in the next render

If you wanted a value to depend on a previously set value, you should do:

setCount(count => count + 1); // same as setCount(0 => 0 + 1)
setCount(count => count + 1); // same as setCount(1 => 1 + 1)
setCount(count => count + 1); // same as setCount(2 => 2 + 1)
console.log(count) // still 0 here, but it will be 3 in the next render

With the above in mind, we can answer the following question:

However when looking at this code sandbox, the values passed to validate (from the validation flow in setFieldTouched are the actual fresh values. I don't see why this is the case...this might very well be a mistaken interpretation of the flow on my side, but that is why I pose this question.

The reason you see different values is that you see the values variable is what it was at the time you clicked the button, and before setFieldValue() has executed, as it behaves like setCount() in the example above. Also, the code sandbox runs Formik 1.3.2, I've explained why that version is special below. You wouldn't see fresh values with the latest version.

How does validate() get the correct data if the values data in React hasn't updated yet?

The value passed to the validate callback of Formik is computed by Formik itself on the fly, as part of the event handler! This is done so that the validation errors can also be calculated without waiting on the rerender of the value. It's basically a performance optimization, and not strictly necessary. Still, let's dive in:

What is useEventCallback()?

You'll see this sprinkled throughout Formik code. All it is is a mild performance optimization - the variables inside that function will always be the variables from the current render, but the function will have a stable referential value. That simply means that setFieldValue from one render is strictly equal (===) to setFieldValue from another render. This is used to optimize some memoization techniques.

In practice, every time you see useEventCallback() you should mentally imagine it's just a normal function;

How does setFieldTouched() ensure that the validation will be run with the "freshest" values?

It doesn't in Formic 2.x! setFieldTouched() runs against whatever values were last rendered.

Warning: The next section explores the behavior of React Class Components. They are not recommended for new projects, React Hooks should be used instead

In Formik 1.x however, setFieldTouched() added validations to be ran as a callback with second parameter to React.Component.setState() - this callback executes AFTER the next render.

So what was happening there was, setFieldTouched() queued this callback, later setFieldValue() updated the value, and then after React rerendered it executed the callback queued by setFieldTocuhed().

How does setFieldValue() work and why does validate() have fresh values?

(This is based on Formik 2.1.6)

I will now inline and comment all of the relevant code that is run in setFieldValue():

const setFieldValue = (field: string, value: any, shouldValidate?: boolean) => {
  // use dispatch() from React.useReducer() to update the value of the field and trigger a next render
  dispatch({
    type: 'SET_FIELD_VALUE',
    payload: {
      field,
      value,
    },
  });

  // from here on out I've inlined the code of validateFormWithHighPriority()

  // set IS_VALIDATING state on the form. 
  // This change executes as part of the above render, alongside the value change!
  dispatch({ type: 'SET_ISVALIDATING', payload: true });

  // create a fresh copy of `values` with the updated field
  const updatedValues = setIn(state.values, field, value);

  Promise.all(
    // validation functions go here; They will run with the `updatedValues`
    // check out the functions `runAllValidations()` and ``
    validate(updatedValues)
  ).then(combinedErrors => {
    // this code is guaranteed to execute AFTER React has rendered the `SET_ISVALIDATING` state above, 
    // and will trigger a NEW React render
    dispatch({ type: 'SET_ISVALIDATING', payload: false });
    dispatch({ type: 'SET_ERRORS', payload: combinedErrors });
  });
}

In effect, what is happening is that setFieldValue() both triggers a new React render, and also calculates what values would be in that new render with setIn(state.values, field, value). It then passes that to validate(), though it does it in a somewhat roundabout way.


I hope this was helpful and that I was able to answer your questions. If there is anything unclear ask away in the comments

VanTanev
  • 1,678
  • 1
  • 10
  • 7
  • 1
    First of all: Thank you very much for your elaborate response! While i must admit that i was aware of most of this (especially the react functionality and how version of 1 formik handled this), i made a **HUGE** blunder in my quest to answer this question. I ignored that the formik version in the playground i adapted was 1.x and not the current 2.x (in which the code i mention is implemented). So thank you very much for pointing this out to me. Your last example is slightly off, since the inline setting of the values only happens if validateOnChange is true ( which i explicitly put to false). – Dean Jul 19 '22 at 09:07
  • 1
    ..in my code sandbox example. But your point with the version really solves this whole paradox and is definitely a fault on my side that i have to thank you for pointing out. I will accept your answer for that point alone, but it might be nice to add a remark on the top that the issue was solved due to the version difference, that i somehow did not care for to check (for anyone, if ever, that makes the same mistake as me). – Dean Jul 19 '22 at 09:11
  • Hey there, @Dean - I'm glad that my answer was helpful after all. The reason I went deep into React render behavior was the following code in the codesandbox: ``` setFieldValue("firstName", e.target.value); console.log("values in between calls", values); setFieldTouched("firstName", true); console.log("values after field touched", values); ``` - it seemed to expect `values` to change, and I wanted to explain that it never does. Cheers! – VanTanev Jul 19 '22 at 14:31
  • Yea i understand. I put it there to make the exact same point for anyone reading :D – Dean Jul 19 '22 at 15:45