ReactJS - warning message 'Hook useEffect has a missing dependency' - javascript

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!

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.

How to access the new value after using useState when outside the body of the function component

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.

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.

Can't reach value in async function with setInterval - 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));
};
};

React Hooks useEffect to call a prop callback from useCallback

I'm trying to make a general-purpose infinite scroller with React Hooks (and the ResearchGate React Intersection Observer). The idea is that a parent will pass down a mapped JSX array of data and a callback that will asynchronously get more data for that array, and when the intersection observer fires because you've scrolled down enough to reveal the loading icon, the callback gets called and more data is loaded.
It works well enough, except one thing: esLint tells me that because I'm calling the getMore function (from the props) inside a useEffect, it must be a dependency of that effect. But because in the parent's callback I'm accessing its data array's length, that array must be a dependency of useCallback there. And then that callback modifies the array.
TL;DR: I'm getting race conditions that cause the async callback to trigger multiple times when it shouldn't, because the callback function reference is changing and then being passed down to the thing that's calling it.
Here's some code to clarify.
The callback in the parent:
const loadData = useCallback(async () => {
if (hasMore) {
const startAmount = posts.length;
for (let i = 0; i < 20; ++i) {
posts.push(`I am post number ${i + startAmount}.`);
await delay(100);
}
setPosts([...posts]);
setHasMore(posts.length < 100);
}
}, [posts, hasMore]);
posts and hasMore are just state variables, with posts being passed down as the data array in props to the child. That function is being passed to the child in props, which has this (getMore is the destructured prop for the callback, isLoading is just a boolean state variable):
useEffect(() => {
if (isLoading) {
(async () => {
await getMore();
setIsLoading(false);
})();
}
}, [isLoading, getMore]);
I'm setting isLoading to true to trigger the effect; but it's also triggering because getMore's reference changes when the parent loads data and the function memoizes. That shouldn't happen. I could just disable esLint for that line, but I assume there's a better solution, and I'd like to know what it is.
Solution: don't use useEffect at all. Just call the loading function directly from the observer and have that set isLoading and call the callback rather than having isLoading trigger the callback.
const loadData = async (observerEntry) => {
if (observerEntry.isIntersecting && !disabled && !isLoading) {
setIsLoading(true);
await getMore();
setIsLoading(false);
}
};

Categories