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