Reputation: 1037
Consider I'm uploading a bunch of files and I'm trying to keep track of the ones in progress so that whenever an upload is finished, I decrement my state until all are finished and finally I change my uploading state to false
:
(Here I made a sleep
function to mimic the uploading process).
import React, { useState} from "react";
const sleep = (ms, dev =1 ) => {
const msWithDev = (Math.random() * dev + 1) * ms
return new Promise(resolve => setTimeout(resolve, msWithDev))
}
function Counter() {
const [, setSortEnabled] = useState(true)
const [count, setCount] = useState(0)
const arr = [1, 2, 3, 4, 5]
const upload = async () => {
await sleep(1000)
setCount(c => c - 1)
console.log(`count state: ${count}`)
if (count === 0) {
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
}
const onClick = () => {
console.warn('Sort set to disabled.')
arr.forEach(item => {
setCount(c => c + 1)
console.error(count)
upload()
})
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;
The problem is in upload
,
function `console.log(\`count state: ${count}\`)`
always logs 0
which means the state has never been updated.
What am I doing wrong here?
Upvotes: 1
Views: 1741
Reputation: 281646
Instead of relying on count, you must use Promise.all
to wait for all files to be uploaded. In your case count is received from closure so even if the state is updated, the count variable remains unaffected.
You can implement the above logic with Promise.all like
function Counter() {
const [, setSortEnabled] = useState(true)
const arr = [1, 2, 3, 4, 5]
const upload = async () => {
await sleep(1000);
}
const onClick = async () => {
console.warn('Sort set to disabled.')
const promises = arr.map(item => {
console.error(count)
return upload()
});
await Promise.all(promises);
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;
UPDATE:
To get around the closure issue of count state, you can make use of refs and useEffect. However its a workaround and should not be preferred
import React, { useState} from "react";
const sleep = (ms, dev =1 ) => {
const msWithDev = (Math.random() * dev + 1) * ms
return new Promise(resolve => setTimeout(resolve, msWithDev))
}
function Counter() {
const [, setSortEnabled] = useState(true)
const [count, setCount] = useState(0)
const arr = [1, 2, 3, 4, 5]
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count ]);
const upload = async () => {
await sleep(1000)
setCount(c => c - 1)
console.log(`count state: ${count}`)
if (countRef.current === 0) {
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
}
const onClick = () => {
console.warn('Sort set to disabled.')
arr.forEach(item => {
setCount(c => c + 1)
upload();
})
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;
Upvotes: 1
Reputation: 53874
Which means the state has never been updated
It does get updated, you are just logging staled value because of closures.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function.
To fix it, use setState
functional updates:
const onClick = () => {
console.warn("Sort set to disabled.");
arr.forEach((item) => {
setCount((c) => {
console.error(c);
return c + 1;
});
upload();
});
};
Upvotes: 3