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

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>
);
};

Related

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));
};
};

_.throttle executing API more than once

I have a TimeLine component that renders posts 5 at a time
and when the user scrolls to the end of the page the throttle function is called.
I am trying to throttle the request to a maximum of once per second but The API is called multiple times
I tried several solutions like:
making a global flag that won't let you use the function unless a certain timeout has passed "It doesn't make sense because I am using the throttle to do that but I tried to check if my implementation is wrong"
declaring the throttle function alone outside the component
TimeLine.js
export default function TimeLine() {
const dispatch = useDispatch();
const posts = useSelector(state => state.postReducer.posts);
const loaded = useSelector(state => state.postReducer.loaded);
const TIMEOUT = 1000;
// Loaded keeps track of loaded posts
if(loaded === 0){
API.Post.getAllPosts(loaded)(dispatch);
}
if(loaded > 5){
if(loaded % 5 === 0) window.scrollTo(0, document.body.scrollHeight * 0.6)
}
window.addEventListener('scroll', () => throttledReq(loaded, TIMEOUT, dispatch));
const classes = useStyle();
return (
<div>
<NewPost />
{(posts && posts.length > 0)?posts.map((post, index) => (
<div key={post._id} className={classes.post}>
<Post
currentIndex={index}
{...post.user}
dispatch={dispatch}
postID={post._id}
postContent={post.text}
/>
</div>
)): false}
</div>
);
};
the Throttle function
const throttledReq = (loaded, TIMEOUT, dispatch) => _.throttle(e => {
const bottomLimit = document.documentElement.offsetHeight - window.innerHeight;
if(document.documentElement.scrollTop === bottomLimit){
API.Post.getAllPosts(loaded)(dispatch);
}
}, TIMEOUT)();
My requests
Every time you scroll, you fire dispatch, which in turn re-runs the component, adding another event listener. This will continue infinitely.
You'll only want the event listener to be added the first time the component is rendered. You can do this with the useEffect hook.
useEffect(() => {
window.addEventListener('scroll', () => throttledReq(loaded, TIMEOUT, dispatch));
}, []);
The empty array at the end means that it doesn't have any dependencies, so it will only run once whenever the component is rendered. Every rerender won't call the callback.
Your loaded value should use a useEffect as well, but in this case with the loaded variable as a dependency. Every time loaded is changed (and on the first render), the callback is fired and the logic assessed.
useEffect(() => {
if (loaded === 0) {
API.Post.getAllPosts(loaded)(dispatch);
}
if (loaded % 5 === 0) {
window.scrollTo(0, document.body.scrollHeight * 0.6)
}
), [loaded]);
Since you're trying to call a certain logic whenever a scroll position is reached, or some element is in view, I recommend you take a look at the Intersection Observer API which eliminates the requirement of a throttled onscroll function.

Having some troubles getting useEffect, useCallback and setTimeout to work together

Trying to implement some custom scroll behaviour for a slider based on the width. It's using useState, to keep track of the current and total pages inside the slider. This is the code I have ended up with trying to acomplise it but it has some unexpected behaviour.
const [isTimeoutLocked, setIsTimeoutLocked] = useState(false)
const handleScroll = useCallback(() => {
const gridContainer = document.querySelector(".grid");
const totalPages = Math.ceil(
gridContainer.scrollWidth / gridContainer.clientWidth + -0.1
);
setTotalPages(totalPages);
const scrollPos = gridContainer.clientWidth + gridContainer.scrollLeft + 2;
if (gridContainer.scrollWidth > scrollPos) {
gridContainer.scrollBy({
left: gridContainer.clientWidth + 20.5,
behavior: "auto",
block: "center",
inline: "center",
});
setCurrentPage(currentPage + 1);
setIsTimeoutLocked(false)
} else {
gridContainer.scrollTo(0, 0);
setCurrentPage(1);
setIsTimeoutLocked(false)
}
},[currentPage]);
useEffect(() => {
if(!isTimeoutLocked){
setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
}
}, [currentPage, displayDate, isTimeoutLocked, handleScroll]);
The problem here is that when the currentPage is displayed in the ui it will reset back to one if the else runs inside the handleScroll function which is totally fine but then it will result back to the previous value instead of going back to 2 when it gets to the next page. Also since I have added the isTimeoutLocked as a dependency to the useEffect since it was asking for it the setTimeout will run more often but I only want to have it as a dependency to get the correct value not so the useEffect runs everytime it changes. The purpose of the isTimeoutLocked is so when you change the content inside the container by changing the current day it has not registered a bunch of timeouts.
You should add a cleanup function to your useEffect. Since you mutate isTimeoutLocked in your useEffect it can force multiple setTimeouts to run and that is probably the result of wierd behaviour.
When using setTimeout inside useEffect it is always recommended to use it with cleanup. Cleanup will run before the next useEffect is triggered, that gives you a chance to bail out of next setTimeout triggering. Code for it is this:
useEffect(() => {
if(!isTimeoutLocked){
const tid = setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
// this is the cleanup function that is run before next useEffect
return () => clearTimeout(tid);
}
You can find more information here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup .
Another thing to be careful is the async nature of setState and batching. Since you are using setState inside a setTimeout, React will NOT BATCH them, so EVERY setState inside your handleScroll will cause a rerender (if it was not inside a setTimeout/async function React would batch them). Since you have alot of interconected states you should look into useReducer.

Debounce in React es6

I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.
Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.
Here is my component code:
const Partners = (props: any) => {
const [query, setQuery] = useState("");
const { partners, totalPages, currentPage } = props.partnersList;
useEffect(() => {
props.getPartners();
}, []);
useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);
const handleQueryChange = (e: any) => {
setQuery(e.target.value);
};
const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
props.getPartners(query, value);
};
return (
<React.Fragment>
<Sidebar />
<MainContent className="iamSearchPartners">
<h1>Partners</h1>
<TextField
className="iamSearchPartners__search_input"
variant="outlined"
size="small"
onChange={handleQueryChange}
value={query}
/>
<PartnersList partners={partners} />
{totalPages > 1 && (
<Pagination
currentPage={currentPage || 1}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
)}{" "}
</MainContent>
</React.Fragment>
);
};
As you can see, I have a useEffect listening to changes to query.
debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.
To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.
const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);
const Partners = (props: any) => {
// omitted ...
useEffect(() => {
myDebouncedFunction(props.getPartners, query);
}, [query]);
// omitted ...
useMemo or putting it into a useState could work as well.
Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.
Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.
I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.
If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.
This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.
const timer = useRef<number>(0);
const lastEventSent = useRef(Date.now());
useEffect(() => {
if (Date.now() - lastEventSent.current < 500) {
// The event was fired too recently, but we still want
// to fire this event after the timeout if it is the last
// one (500ms in this case).
timer.current = setTimeout(() => {
lastEventSent.current = Date.now();
props.getPartners(query)
}, 500);
} else {
// An event hasn't been fired for 500ms, lets fire the event
props.getPartners(query)
lastEventSent.current = Date.now();
// And if the user was typing, there is probably a timer, lets clear that
clearTimeout(timer.current);
}
// Cleanup the timer if there was one and this effect is reloaded.
return () => clearTimeout(timer.current);
}, [query]);

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