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.
Related
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.
Consider the following code
const App = () => {
const [errorMsgs, setErrorMsgs] = useState([])
const onButtonPressed = () => {
fetchErrors(setErrorMsgs)
if (!errorMsgs.length) {
// Procced and do somethings
}
}
}
function fetchErrors(setErrorMsgs) {
// if some condition is true:
setErrorMsgs(errorMsgs => [...errorMsgs, "some error"])
// if some other condition is true:
setErrorMsgs(errorMsgs => [...errorMsgs, "some other error"])
}
As far as I understand, when using setErrorMsgs, it doesn't immediately update the state, and this cannot be solved by using await or .then() because setErrorMsgs doesn't return a promise, therefore, the condition will never be true in this sample.
So what should I do so the condition runs after the state updates? (I don't want the condition to run every time the errorMsgs changes.)
Edit: To clarify:
The problem, when I call fetchErrors function, it checks some conditions and might append one, or more errors to errorMsgs. When I check to see if errorMsgs is an empty array, the condition will be true even if the fetchErrors adds anything to the array.
You can't assume that when you change state the variable had it's value changed, but react have a correct way to handle it, by using useEffect, so
const [state, setState] = useState(false)
const onButtonPressed = () => {
setState(true)
}
useEffect(() => {
// here you can do some stuff
},[state])
You can redesign your fetchErrors function, to return the value from the condition, instead of running setter function, then you can do what #RobinZigmond has suggested:
const newErrors = fetchErrors();
setState(newErrors);
if (!newErrors.length) {...}
this way you know for sure what your next state will be like.
Desired Outcome:
I am getting a script's status from an API and then checking what the status is.
If the status = 'XYZ', I need to call the getStatus function again (using setInterval),
else I just need to return/do nothing.
Issue:
The major issue is that clearInterval() is not working and the getStatus() function is getting called after the mentioned interval (10 seconds here) again and again, EVEN THOUGH the status IS NOT EQUAL to 'XYZ'.
Please help out. Thanks in advance.
const [intervalId, setIntervalId] = useState();
useEffect(() => {
getStatus();
}, []);
const getStatus = async () => {
await getScriptStatus()()
.then(response => {
if (response.data[0].status == 'XYZ') {
setIntervalId(
setInterval(() => {
getStatus();
}, 10000)
);
}
else
return () => clearInterval(intervalId);
})
.catch(error => {
errorMessage(error);
});
UPDATE:
Using React.useRef() instead of useState() for intervalId and clearing interval before using setInterval() in the 'if' condition worked or me.
Thank you for the helpful answers.
Use setTimeout instead of using setInterval.
For more details visit : Documentation on setTimeout
intervalId doesn't need to be a "state", rather store is as a ref and not a state.
Notice I changed your function from using .then().catch() to using async/await.
I think that the problem you're facing is caused because React State is asynchronous - the setIntervalId didn't update the intervalId yet, when you're trying to already access intervalId's new value
const intervalId = useRef(null);
const getStatus = async () => {
try {
const response = await getScriptStatus()()
if (response.data[0].status == 'XYZ') {
intervalId.current = setInterval(getStatus, 10000);
}
else return () => clearInterval(intervalId.current);
} catch (error) {
errorMessage(error);
}
}
Another suggestion:
The getStatus function should not act as a useEffect-function, because it's very confusing to see that getStatus returns a function.. and then you need to trail back to "AH it's a useEffect-function.. and the function returned is a cleanup function.. " - not good.. .
so.. My Suggested Options for that:
Either change the getState function to only getStatus.. and then when you call it from inside the useEffect- also handle the case in which you want to return a function (cleanup function)
Less suggested: Have all the content straight inside the useEffect. And since you want to use async/await -> create a self-calling-async-function: useEffect(()=>{ (async () => { await func(); })(); }, []);
Update:
In addition to all that ^
You really don't need the setInterval (like others have suggested). setTimeout will do just fine. But you might still want the clearTimeout (changed from clearInterval) if the functio might be called on different occasions
Main problem here is most probably that you are installing new intervals recursively: function installs interval that calls function that installs interval etc, effectively producing cascade of intervals. These are more or less in-revocable, because their IDs live ephemerally in function scope that installed them and are then thrown away.
You can observe it with slightly modified version of your code in this sandbox.
Change return () => clearInterval(intervalId); to this clearInterval(intervalId);.
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));
};
};
I'm getting the message in my console:
React Hook useEffect has a missing dependency: 'calculateTotalProductsPrice'. Either include it or remove the dependency array
The code is working but I dont know if is this is the proper way of implementing useEffect
Here I have a function that calculates the total price of my products and im calling it in my useEffect hook:
// Get all products the user added from my redux store
const allProductsAddedByUser = useSelector(state => state.createCampaign.campaign_products)
function calculateTotalProductsPrice() {
const productsArray = Object.values(allProductsAddedByUser)
const totalPrice = productsArray.reduce(function (prev, cur) {
return prev + cur.quantity * 125.0
}, 0)
return totalPrice
}
useEffect(() => {
calculateTotalProductsPrice()
}, [allProductsAddedByUser])
And in my html, I'm calling the function like this:
<span>€ {calculateTotalProductsPrice()}</span>
If I put the calculateTotalProductsPrice() function inside my useEffect hook, the warning from console is gone, but then I can't call my function like I was doing, I'm getting the error 'calculateTotalProductsPrice' is not defined. My function is now inside the useEffect hook, why cant I call her from outside?
// Get all products the user added from my redux store
const allProductsAddedByUser = useSelector(state => state.createCampaign.campaign_products)
useEffect(() => {
function calculateTotalProductsPrice() {
const productsArray = Object.values(allProductsAddedByUser)
const totalPrice = productsArray.reduce(function (prev, cur) {
return prev + cur.quantity * 125.0
}, 0)
return totalPrice
}
calculateTotalProductsPrice()
}, [allProductsAddedByUser])
// Call function from my html
<span>€ {calculateTotalProductsPrice()}</span>
// Error calculateTotalProductsPrice is not defined
About the warning message:
There are 2 scenarios in this case.
The first is which is the current scenario when you need the calculateTotalProductsPrice function somewhere else in your code then you need to create outside of useEffect. One addition if that's the case which is passing it to so called dependency array, just like the following:
function calculateTotalProductsPrice() {
// ... your calculation code
}
useEffect(() => {
calculateTotalProductsPrice()
}, [calculateTotalProductsPrice])
In this case the warning message won't be shown again and in the same time it will be accessible in JSX as well, just like <span>€ {calculateTotalProductsPrice()}</span>.
Second is once you don't need the calculateTotalProductsPrice function outside of useEffect then you can create inside of the callback as you did in your second example.
About the accessibility of the function:
And the reason why the function calculateTotalProductsPrice is not accessible outside once you declare in useEffect is becasue of JavaScript scope. The function only accessible inside of useEffect hook because of closure, from the documentation:
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. In JavaScript, closures are created every time a function is created, at function creation time.
Read further here:
JavaScript scope
Closures
I hope this helps!