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.
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
Related
I've coded simple timer, but when I try to console.log(time)(It is in handleStart), I get the same output 0, even if setTime() is called.
Here's the part of code, where I console.log(time) (It is in handleStart, you should click the Start button in order to see console.log):
const handleStart = () => {
setIsCounting(true);
timerIntervalRef.current = setInterval(() => {
console.log(time);
setTime((prevState) => {
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
}, 1000);
};
Link to Sandbox
Please, explain me, why it works that way, cause I think, that, the callback in setInterval has a reference to a time, which is defined above, so every time this callback gets called, it should climb through closure to the the point, where time is defined, so it should get renewed value.
time is a local const, which your interval function closes over. As a const, it can never change, so you're always logging out the original value. Even if you used let the behavior would be the same, because calling setTime does not change the value in the local variable. Rather it asks react to rerender your component. On that new render, a new local variable will be created with the new value, but code in the old render (including the code in the setInterval) still only has the old variable in its scope and cannot access the new one.
If you'd like to verify that the component is rerendering, you can move your log statement into the body of the component.
console.log('rendering with', time); // moved outside of handle start
const handleStart = () => {
// ... normal handleStart function, minus the console.log
}
Or if you want a log statement at the time you set the state, you could move it inside the set state function, since that gets passed the latest value of the state and doesn't depend on a closure variable:
setTime((prevState) => {
console.log(prevState);
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
inside of settime you are getting the renewed value (prevState), the "time" is referencing to the initial time, consoling will obviously refer to initial value, I think you should console.log(prevState)
I am using react app context to store an array of "alerts objects" which is basically any errors that might occur and I would want to show in the top right corner of the website. The issue I am having is that the context is not being up to date inside a timeout. What I have done for testing is gotten a button to add an alert object to the context when clicked and another component maps through that array in the context and renders them. I want them to disappear after 5 seconds so I have added a timeout which filters the item that got just added and removes it. The issue is that inside the timeout the context.alerts array seems to have the same value as 5 seconds ago instead of using the latest value leading to issues and elements not being filtered out. I am not sure if there's something wrong with my logic here or am I using the context for the wrong thing?
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts([
...context.alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts(alerts => [
...alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
This should fix it. Until react#17 the setStates in an event handler are batched ( in react#18 all setStates are batched even the async ones ), hence you need to use the most fresh state to make the update in second setAlerts.
To be safe it's a good practice using the cb syntax in the first setState as well.
I think the fix would be to move context.setAlerts(...) to a separate function (say removePopupFromContext(id:string)) and then call this function inside the setTimeout by passing the errorPopup.Id as parameter.
I'm not sure of your implementation of context.setAlerts, but if it's based on just setState function, then alternatively, you could do also something similar to how React let's you access prevState in setState using a function which will let you skip the creation of the extra function which may lightly translate to:
setContext(prevContextState =>({
...prevContextState,
alerts: prevContextState.alerts.filter(your condition)
)})
I use useSWR on a project, and I really like it, but am having an issue with revalidation.
I have a HOC to encapsulate form components in order to not render them until the validation has been run, because if the form component gets initialized with cached data it won't be reupdated after the revalidation, something like this:
useEffect(() => {
if (isValidating) {
setNumberOfTimesThatHasBeenValidating((n) => n + 1);
}
}, [isValidating, setNumberOfTimesThatHasBeenValidating]);
const requiredNumberOfTimes = 1;
const hasValidatedRequiredNumberOfTimes =
(numberOfTimesThatHasBeenValidating >= requiredNumberOfTimes &&
!isValidating) ||
numberOfTimesThatHasBeenValidating > requiredNumberOfTimes;
if (!hasValidatedRequiredNumberOfTimes) {
return <>{fallback}</>;
}
return <RealComponent />
The idea is that when the isValidating turns true and then false, a validation has happened. I previously had implemented with a single boolean state variable "hasValidatedAtLeastOnce" and I thought it worked fine, but then I noticed that even when I use mutate() the isValidating becomes true and false one time before the fetcher function runs. What is that supposed to mean? Why is it that isValidating becomes false if the fetcher hasn't been run? What is being validated then?
Is this behaviour consistent enough that I can safely implement this just raising the required number of times to 2? Setting "shouldRevalidate" on mutate to true doesn't seem to make a difference either.
According to the react docs at https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect when using the useEffect dependency array, you are supposed to pass in all the values used inside the effect.
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.
I don't know how the hook works behind the scenes, so I'm going to guess here.
Since the variables inside the closure might go stale, that would imply that the function is cached somewhere. But why would you cache the function since its not being called unless the dependencies changed and the function needs to be recreated anyways?
I've made a small test component.
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
console.log("b", b);
}, [a]);
return (
<div>
<div>App {a}</div>
<button
onClick={() => {
setA(a + 1);
}}
>
AddA
</button>
<button
onClick={() => {
setB(b + 1);
}}
>
AddB
</button>
</div>
);
}
You can try it out here: https://codesandbox.io/s/react-hooks-playground-forked-m5se8 and it works just fine, with no stale values. Can someone please explain what I'm missing?
Edit:
After feedback that my question is not entirely clear, adding a more specific question:
When after a page load I click on AddB button and then click on AddA button, value displayed in console is 1. According to the docs, I should get a stale value (0). Why is this not the case?
When after a page load I click on AddB button and then click on AddA
button, value displayed in console is 1. According to the docs, I
should get a stale value (0). Why is this not the case?
The reason why you don't see stale value in that case is that when you click AddA a re-render happens. Inside that new render since value of a is different from previous render, the useEffect will be scheduled to run after react updates the UI (however the values it will reference will be from current render - because the function passed to useEffect is re-created each time one of its dependencies change, hence it captures values from that render).
Due to above reasons, that is why you see fresh value of b.
But if you had such code
React.useEffect(() => {
const timer = window.setInterval(() => {
setB(b + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
b would be a stale closure. Since useEffect didn't re-run, b always has value from the render it was created in, that is the first render and above code wouldn't work as expected, which you can check yourself.
I will add this comment by Patrick Roberts from above because it explains well IMHO what react docs may mean when they say "Otherwise, your code will reference stale values from previous renders":
the problem is that you had to click on AddA button in the first
place. Between the time you click AddB and AddA, the effect references
b from a stale closure
I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}