Can't reach value in async function with setInterval - JavaScript - javascript

I'm trying to do automated card pull started with button, and stop it when croupierCount reach 17, using setInterval. Value of croupierCount changes (<div> displays this count) with this function below, but when I'm trying to reach this value inside function to stop interval, it's logged value is 0. Can you help me solve this?
const TheGame = () => {
const [croupierCount, setCroupierCount] = useState(0);
const [croupierHand, setCroupierHand] = useState([]);
const onStandHandler = () => { // triggered with button
const croupierInterval = async () => {
let card = await fetchCard(deck); // fetching new card with every iteration (works)
croupierHand.push(card); // pushes fetched card (works)
if (card[0].value === 'ACE') {
setCroupierCount((prevCount) => prevCount + 11);
}
else if (card[0].value === 'JACK' || card[0].value === 'QUEEN' || card[0].value === 'KING') {
setCroupierCount((prevCount) => prevCount + 10);
}
else {
setCroupierCount((prevCount) => prevCount + Number(card[0].value));
};
// croupierCount is changing (I'm displaying it in div)
console.log(croupierCount); // croupierCount = 0, I don't know why.
if(croupierCount > 17) {
clearInterval(startInterval);
};
}
const startInterval = setInterval(croupierInterval, 1000);
};
};

You seem to miss an important point: using const here...
const [croupierCount, setCroupierCount] = useState(0);
... makes croupierCount a constant value, regardless of how many times setCroupierCount is called. This variable cannot be updated directly: what you see as its update is actually changes in React internal state, represented by the same name when the component is rerendered - and render function is called again.
This immutability is both a blessing and a curse typical to hook-based functional components.
Here's what happens here:
when component is rendered, TheGame function is called first time. Its useState call initializes both value and the corresponding setter as a part of internal React state tied to this component's instance.
Those values are returned from useState function - and are stored in local variables (constants!) of TheGame function, croupierCount and setCroupierCount. What's important - and often missed - is that these particular variables are created anew each time TheGame function is called!
then onStandHandler function is created, having both aforementioned local variables available as part of its scope.
at some point, onStandHandler function is triggered (when user presses a button). It creates yet another function, croupierInterval, which should fetch data first, then update the state by calling setCroupierCount with result of this fetch.
There are two problems with this function, though.
First, all croupierInterval sees is values of current croupierCount and setCroupierCount variables. It cannot magically 'peek' into which values those variables will carry when rerender is triggered and TheGame function is executed next time - as those will be new variables actually!
But there's a bigger problem you seem to miss: setInterval doesn't play nicely with fetch (or any async action). Instead of waiting for the processing of that action, you just make JS trigger this function periodically.
Not only this messes up with an expected delay (slow down fetch so that it takes 10 seconds, then see what happens), but there's an actual bug here: as clearInterval(startInterval) doesn't stop processing all the parts of a function that follow await fetchCard(deck), in the worst case, your Croupier might go way above 17.
This is for 'why' part, but what's on 'how to fix'? There are several things worth trying here:
avoid using setInterval in functional components like a plague: there are often far better replacements. In this case in particular, you should've at least tied setting up calling croupierInterval to the previous call's completion
useEffect whenever you want something to modify your state indirectly as some kind of side-effect. Not only this makes your code easier to read and understand, but also lets you clear out the side effects of side effects (like timeouts/intervals set)
don't forget to handle human errors, too: what should happen if a user mistakenly double-clicks this button?

I'm posting my solution, if someone encounters a similar problem.
const isMount = useRef(false);
...
useEffect(() => {
if (isMount.current && croupierCount !== 0) {
if (croupierCount < 17) {
setTimeout(() => {
onStandHandler();
}, 1000);
}
}
else isMount.current = true;
}, [croupierCount]);
const onStandHandler = async () => {
let card = await fetchCard(deck, 1);
croupierHand.push(card);
if (card[0].value === 'ACE') {
setCroupierCount(prevCount => prevCount + 11);
}
else if (card[0].value === 'JACK' || card[0].value === 'QUEEN' || card[0].value === 'KING') {
setCroupierCount(prevCount => prevCount + 10);
}
else {
setCroupierCount(prevCount => prevCount + Number(card[0].value));
};
};

Related

I set new state but it remains unchanged in next line function call

I have piece of a code that I don't entirely understand why it acts like that.
const handleLoadMore = () => { setPageNumber(pageNumber+1); fetchData(pageNumber); };
State pageNumber remains same as I didn't change the state. Outside of the handleLoadMore function increments. I kind of understand that it won't increment function body. So, is there anyway to tell fetchData(pageNumber) to wait for state to update.
I tried to find on google how to solve this kind of a problem. I tried setTimeout function but it didn't help.
Do this:
const [pageNumber, setPageNumber] = React.useState(0);
const handleLoadMore = () => {
setPageNumber(number => number + 1);
}
React.useEffect(() => {
fetchData(pageNumber);
},[pageNumber])
You state setter function is happening asynchronously (this is by default with React), meaning it is not executed until the call stack is clear, so after your fetchData call has been made.
Presumably, the handleLoadMore function is event-based, like a button onClick.
So instead, pass the pageNumber + 1 to the fetchData function directly. No need for useEffect.
const handleLoadMore = () => {
setPageNumber(previousNum => previousNum + 1)
fetchData(pageNumber + 1)
}
Important Side-note
When updating state with a reference to itself, it is best practice to use the function argument syntax, to ensure you are not updating based on a stale reference to the state.

Defining MainButton.onClick() for Telegram Webapp correcly

I am trying to define window.Telegram.WebApp.MainButton.onClick() functionality to use the most recent values.
Lets see through code:
// selectedRegions is an array of strings
const [selectedRegions, setSelectedRegions] = React.useState([""]);
React.useEffect(() => {
window.Telegram?.WebApp.MainButton.onClick(() => {
// window.Telegram.WebApp.sendData(selectedRegions);
alert("main button clicked");
});
}, [selectedRegions]);
Now as I update the selectedRegions, this useEffect is called for the number of times the state changes which also updates the MainButton.onClick() functionality. Then at the end, when the MainButton is pressed, in this case, alert() is shown the number of times the state was updated, eg, if the selectedRegions contain 3 values, the alert will be shown 3 times.
What I want is to somehow define this onClick() once or the functionality executed only once, so that I can send the selectedRegions only once with the most recent values back to the bot.
UPDATE 1: After looking into the function onEvent() in telegram-web-app.js file,
function onEvent(eventType, callback) {
if (eventHandlers[eventType] === undefined) {
eventHandlers[eventType] = [];
}
var index = eventHandlers[eventType].indexOf(callback);
if (index === -1) {
eventHandlers[eventType].push(callback);
}
};
It seems to my understanding that if the callback is present in eventHandlers["webView:mainButtonClicked"], then it will not do anything, otherwise just push it into the array.
What I fail to understand is that somehow the callbacks are different, that's why they get appended to the array, justifying the multiple calls to it.
However, I am trying to use MainButton.offClick() to remove the from eventHandlers array, have not succeeded yet. If anyone has done so, it would be highly appreciated.
I have faced exactly same issue. I had some buttons that updates state and on main button click I wanted to send that state, but as you mentioned telegram stores all callbacks in array and when you call sendData function, web app gets closed and only first callback is executed. If you tried to remove function with offClick, I think that didn't worked, because callbacks are compared by reference, but function in component was recreated by react when component is re-rendered, so, that's why offClick failed to remove that function. In this case solution would be like this
const sendDataToTelegram = () => {
window.Telegram.WebApp.sendData(selectedRegions);
}
useEffect(() => {
window.Telegram.WebApp.onEvent('mainButtonClicked', sendDataToTelegram)
return () => {
window.Telegram.WebApp.offEvent('mainButtonClicked', sendDataToTelegram)
}
}, [sendDataToTelegram])
If you are not familiar with this syntax - return in useEffect is callback that is called before new useEffect is called again
Or you can solve this also with useCallback hook to recreate function only when selected region state is changed. Like this:
const sendDataToTelegram = useCallback(() => {
window.Telegram.WebApp.sendData(selectedRegions);
}, [selectedRegions])
useEffect(() => {
window.Telegram.WebApp.onEvent('mainButtonClicked', sendDataToTelegram)
return () => {
window.Telegram.WebApp.offEvent('mainButtonClicked', sendDataToTelegram)
}
}, [sendDataToTelegram])
I use Angular with typescript and I ran into this problem too. But I finally found a solution: for some reason if I pass a callback like this -
"offClick(() => this.myFunc())" the inner "indexOf" of the "offEvent" cannot find this callback and returns "-1".
So I ended up with:
setOnClick() {
let f = () => this.myFunc;
window.Telegram.WebApp.MainButton.onClick(f);
}
setOffClick() {
let f = () => this.myFunc;
window.Telegram.WebApp.MainButton.offClick(f);
}
myFunc() {
console.log('helloworld');
}
Hope this helps.

Using request animation frame in React

I'm reading this article and I'm not sure I understand how the final hook works.
Here is the code:
const useAnimationFrame = (callback) => {
const requestRef = useRef();
const previousTimeRef = useRef();
const animate = (time) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
}
and used for example in this way:
const [count, setCount] = useState(0);
useAnimationFrame((deltaTime) => {
setCount((prevCount) => {
return prevCount + 1;
});
});
Ok, the goal is have a number value that is incremented every frame.
I can explain what is happens running this code:
the component create a local state with useState(0)
then the useAnimationFrame hook is called using this callback as parameter:
(deltaTime) => {
setCount((prevCount) => {
return prevCount + 1;
});
}
the function takes a number as input and increment ste state value of one each time it is called.
useAnimationFrame is a function that takes another function as a parameter (a callback). It creates two refs. At the first time it is executed (because of the []) it calls the useEffect. It saves in requestRef.current the timestamp the requestAnimationFrame returns. The requestRef.current calls the animate function that computes the delta time between the request animation frames (the previous and the current) and then call the callback with this value so it calls the setCount. Then it updates the current refs values and recall the requestAnimationFrame.
So the cycle should be:
component
> count = 0
useAnimationFrame <--------------+
> requestRef = ? |
> previousTimeRef = ? |
useEffect |
animate |
> deltaTime = delta#1 |
> count = 1 |
> previousTimeRef.current = time#1 |
> requestRef.current = time#2 -------+
> requestRef.current = timestamp#1
Am I wrong?
It might be helpful to track the function signatures of requestAnimationFrame and cancelAnimationFrame.
requestAnimationFrame takes a single argument, a callback function. The callback function itself receives a single timestamp argument (DOMHighResTimeStamp)
cancelAnimationFrame takes a single argument, the id of the requestAnimationFrame which you want to cancel.
So time in the animate callback function is the single argument received via the api, a DOMHighResTimeStamp similar to the one returned by performance.now(), indicating the point in time when requestAnimationFrame() starts to execute callback functions.
const animate = (time) => {
This is a check to see if the hook has already run 1x. If it has, update the parent React scope with the new time minus the previous time
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime);
}
Once the hook is confirmed as having run, save the DOMHighResTimeStamp for future calculations
previousTimeRef.current = time;
After this, it gets a bit interesting and I am not sure this is best approach. It may even be a bug. The code sets a new listener and updates the ref with the latest id, based on the result of a new invocation.
Just from reading the code, I am not sure the original listener ever gets cancelled. I suspect it is not.
/// this is an id
requestRef.current = requestAnimationFrame(animate);
I don't have access to a running version, but I would suggest removing the requestRef.current altogether and see if the clean-up happens as expected when the useEffect clean-up happens e.g.
useEffect(() => {
const id = requestAnimationFrame(animate);
return () => cancelAnimationFrame(id);
}, []);
This will also simplify the embedded refs as well to make reading a bit more clear.

Too many re-renders even though I want an infinite loop

I'm trying to create a simple idle game so that I can dive deeper into web development.
However, react is complaining about too many re-render, but I do want to re-render every second.
This is the code I have at the moment.
> const Game = () => { const [resourceCopper, setResourceCopper] =
> useState(0);
>
> const gatherCopper = () => {
> setResourceCopper(resourceCopper + 1); };
>
> setInterval(gatherCopper(), 1000);
>
> return (
> <section className="main-window-container">
> <section className="left-container">
> <h3>Resources:</h3>
> <p>Copper: {resourceCopper}</p>
> </section>
The immediate issue is that you're immediately executing gatherCopper, which immediately updates the state, rerender, and will cause an infinite loop.
You should remove the () behind gatherCopper in the setInterval call.
However, this code is very leaky, and because of the way React works you will create a new interval for every time the component renders. This will not work as expected.
The interval should be moved to a a React hook (i.e. useEffect), perhaps there's even hooks which wrap setInterval. A google search will probably come up with some good examples.
With React hooks you'll be able to start the interval when the component mounts for the first time, and you can also tell it to cancel the setInterval when the component unmounts. This is important to do.
update: Example with annotations (sandbox link)
const Game = () => {
const [resourceCopper, setResourceCopper] = useState(0);
useEffect(() => {
// We move this function in here, because we only need this function once
const gatherCopper = () => {
// We use the 2nd way of calling setState, which is a function that will
// receive the current value. This is so that we don't have to use the resourceCopper
// as a dependency for this effect (which would fire it more than once)
setResourceCopper(current => current + 1);
};
// We tell the setInterval to call the new gatherCopper function
const interval = setInterval(gatherCopper, 1000);
// When an effect returns a function, it will be used when the component
// is cleaned up (a.k.a. dismounts). We want to be neat and cancel up the interval
// so we don't keep calling this on components that are no longer there
return () => clearInterval(interval);
}, [setResourceCopper]);
return (
<section className="main-window-container">
<section className="left-container">
<h3>Resources:</h3>
<p>Copper: {resourceCopper}</p>
</section>
</section>
);
};

React useEffect Warning: Maximum update depth exceeded. Is there a way around this?

As shown in the code below, I am using the useEffect hook to watch for changes to a percentage variable, then set a timer to increment that variable in 1 second. The callback will occur as soon as the page loads, kicking the process off.
The percentage variable is used to render a line chart that increments until a passed value is reached. (It basically shows a % pass rate, but on load the bars will move across the screen up to their %.)
Everything works fine, but the console flags the maximum update depth warning which points out it's trying to prevent an infinite loop when i'm using setState inside useEffect. I get the point here, but I know I'll never pass in a value above 100 and so there will never be an infinite loop. I also need the recursive functionality to have my charts appear to populate dynamically.
Even if I decided to ignore the warning, it's creating error in my testing if I mount the component.
Is there a way I can tell react that this won't be an infinite loop and get the error to go away? Or do I need a different approach?
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
let timer = null;
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer);
return;
}
timer = setTimeout(setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;
Thanks for any help
In timer = setTimeout(setPercent(newPercent), 1000);
setPercent(newPercent) will be called immediately. setTimeout requires a function (instead of a function call):
timer = setTimeout(() => setPercent(newPercent), 1000);
Another thing to keep in mind is that timer will be defined on each render, thus clearTimeout won't have any effect. To avoid this, you can create a ref so that the value will be remembered. Like so:
const timer = React.useRef()
using/setting the value is done to the current property
timer.current = ...
Working example:
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
const timer = React.useRef();
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer.current);
return;
}
timer.current = setTimeout(() => setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;

Categories