Reputation: 594
I'm trying to show a loader for 1.5s after submitting a form, then stop the loader and submit the form.
so I set a state variable isSubmitting
:
const [isSubmitting, setIsSubmitting] = useState(false);
isSubmitting
is false
by default, and upon submitting, I turn it to true
, after that I set a timeout to set it back to false
. but somehow, it does not get set back to false
see commented code
const onSubmit = (e) => {
e.preventDefault();
setIsSubmitting(!isSubmitting);
setTimeout(() => {
createProfile(formData, history, edit);
console.log(isSubmitting);// false - why?? its been set to true before the timeoute
setIsSubmitting(!isSubmitting);
console.log(isSubmitting);// false - if the previous log was false, this should of been true
}, 1500);
};
console.log(isSubmitting) // true - ??? it did not get set back to false
Upvotes: 0
Views: 64
Reputation: 12637
isSubmitting
isfalse
by default, and upon submitting, I turn it totrue
, after that I set a timeout to set it back tofalse
. but somehow, it does not get set back tofalse
see commented code
That is how closures work
but how come in plain js, I dont have this problem? sounds like it has to do with useState?
Yes, in plain JS you have the same problem. I'll show you:
let storedValue = false;
function setStoredValue(newValue) {
storedValue = newValue
}
// useState() is basically this; more or less.
// The place where `storedValue` and `setStoredValue` are kept is differrent,
// but that's not relevant to the issue at hand.
function gimmeTheValueAndASetter() {
return [
storedValue,
setStoredValue
]
}
function Element(label) {
// you create a local **constant** copy of the state at this moment
const [localValue, setStoredValue] = gimmeTheValueAndASetter();
// and all your logs here are based off of this local **constant**!
console.log(label, "#1 local:", localValue, " stored:", storedValue);
setStoredValue(!localValue);
console.log(label, "#2 local:", localValue, " stored:", storedValue);
setTimeout(() => {
console.log(label, "#3 local:", localValue, " stored:", storedValue);
setStoredValue(!localValue);
console.log(label, "#4 local:", localValue, " stored:", storedValue);
}, 100);
}
// render Element
Element("first render");
console.log("after first render:", storedValue);
Element("second render");
.as-console-wrapper{top:0;max-height:100%!important}
Back to your code. Instead of flipping the state, better set the explicit value. This is also simpler to reason about:
const onSubmit = (e) => {
e.preventDefault();
// we don't want to submit while another submit is still in progress
// right?
if(isSubmitting) return;
setIsSubmitting(true);
setTimeout(() => {
createProfile(formData, history, edit);
setIsSubmitting(false);
}, 1500);
};
Ain't this simpler to reason about than what you had before? isSubmitting
was false
then I've flipped it to true
, so now I have to flip it again to get back to false
, ...
Upvotes: 0
Reputation: 177
Maybe little overkill byt you can handle this by using Promise
const handleRequest = () => new Promise((resolve, reject) => {
setTimeout(() => {
createProfile(formData, history, edit)
.then(response => resolve(response))
.catch(error => reject(error));
}, 1500);
}
const onSubmit = (event) => {
event.preventDefault();
setIsSubmitting(true);
handleRequest().finally(() => setIsSubmitting(false);
};
And maybe second overkill: I'm not sure why you need to submit data after 1.5s, not immediately but you can have both 1.5s loader and request sended just after submit by creating some dummy Promise
and using Promise.all
like this:
const dummyPromise = () => new Promise((resolve, reject) => {
setTimeout(() => resolve(), 1500);
}
const onSubmit = (event) => {
event.preventDefault();
setIsSubmitting(true);
Promise.all([
dummmyPromise(),
createProfile(formData, history, edit)
])
.then(response => ...handle successfull response)
.catch(error => ...handle request error)
.finally(() => setIsSubmitting(false));
};
The thing is you don't know how long your request will last so by setup like this you are prepared for different situations: sumbmit request is sent immidately and Promise.all
will resolve after all promises resolves so if your request resolves very fast you will still have 1.5s loader, on the other hand if request will last longer than 1.5s you will still have active loader indicating that request is not finished yet. Like I said overkill in your situation but I hope my comment will contain some nice inspirations how to improve handling async requests ;)
Upvotes: 0
Reputation: 1920
The state update using the updater provided by useState hook is asynchronous, and will not immediately reflect the updated changes.
i.e
console.log(isSubmitting); // false
setIsSubmitting(!isSubmitting);
console.log(isSubmitting); // false
Instead you should useEffect,
const onSubmit = (e) => {
e.preventDefault();
setIsSubmitting(!isSubmitting);
};
useEffect(() => {
// action on update of isSubmitting
if(isSubmitting) {
setTimeout(() => {
createProfile(formData, history, edit);
setIsSubmitting(!isSubmitting);
}, 1500);
}
}, [isSubmitting]);
Upvotes: 1
Reputation: 200
That is how closures work in JS. The functions passed to setTimeout will get the isSubmitting
variable from the initial render, since isSubmitting
is not mutated.
You can get a function as an argument instead of prop to setIsSubmitting
That means that isSubmitting
is gonna be updated value.
setTimeout(() => {
setIsSubmitting(isSubmitting => !isSubmitting);
}, 1500);
Hope it will work
Upvotes: 0