How I can use useState hook in an infinite loop? - reactjs

I'm beginner in React and I want create a component that is in constant motion, incrementing its style property transform:translate, but the increment isn't what I expected. What's happening?
This is my code:
function Point() {
const [countX, setCountX] = useState(0);
const [countY, setCountY] = useState(0);
setInterval(() => {
setCountX((count) => count + 1);
setCountY((count) => count + 1);
console.log(countX+":"+countY);
}, 500);
const styles = {
transform: `translate(${countX}px,${countY}px)`,
};
return <PointStyle style={styles} />;
}
And the output in the console from console.log(countX+":"+countY) is as follows:
0:0
1:1
0:0
2:2
1:1
4:4
0:0
5:5
2:2
7:7
1:1
9:9
4:4
10:10
11:11
0:0
12:12
5:5
14:14
2:2
7:7

Every time your component re-renders, it is creating an interval. Also, the previous intervals are not getting cleaned up. So, all your intervals are updating the count at the same time.
You need to move the setInterval into a useEffect and also cleanup the interval.
useEffect(() => {
const interval = setInterval(() => {
setCountX((count) => count + 1);
setCountY((count) => count + 1);
console.log(countX+":"+countY);
}, 500);
return () => clearInterval(interval)
}, [])

Each time you call one of your setState functions (setCountX, or setCountY) the component will end up being rerendered. This will cause the setInterval to be called repeatedly creating many intervals running out of sync all attempting to update the X and Y position of your point.
To fix this you need to put your setInterval code inside of a useEffect.
const Point = () => {
const [countX, setCountX] = useState(0);
const [countY, setCountY] = useState(0);
// The below useEffect will be triggered on component mount and
// whenever a value in the dependency array changes (an empty
// dependency array is only on mount, such as this example).
//
// The function returned from the useEffect is a clean up function
// that will be run whenever a value in the dependency array changes
// or when the component unmounts
useEffect(() => {
// Create interval to change point X and Y, ensuring to store
// the returned ID so that the interval can be cleared later.
const intervalId = setInterval(() => {
setCountX((count) => count + 1);
setCountY((count) => count + 1);
console.log(countX + ":" + countY);
}, 500);
// clean up function created in the current closure will
// keep hold of previously stored intervalId value.
return () => {
// Clear interval (in this example only run on component unmount)
clearInterval(intervalId);
}
}, [] /* Dependency Array */)
const styles = {
transform: `translate(${countX}px,${countY}px)`,
};
return <PointStyle style={styles} />;
}

Related

How do I test for DOM changes created by a setInterval function when using Jest/React testing library?

I created a simple countdown component that I would like to test is working correctly with React testing library. Everything works as expected in a browser but when testing the rendered output never advances more than one second regardless of the values used.
Component (custom hook based on Dan Abramovs blog post):
import React, { useEffect, useRef, useState } from "react"
import TimerButton from "../TimerButton/TimerButton"
// custom hook
function useInterval(callback, delay, state) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (state.isOn) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [state])
}
const Timer = () => {
const initialState = {
minutes: 25,
seconds: 0,
isOn: false,
}
const [timerData, setTimerData] = useState(initialState)
useInterval(
() => {
if (timerData.seconds === 0) {
if (timerData.minutes === 0) {
setTimerData({ ...timerData, isOn: false })
} else {
setTimerData({
...timerData,
minutes: timerData.minutes - 1,
seconds: 59,
})
}
} else {
setTimerData({ ...timerData, seconds: timerData.seconds - 1 })
}
},
1000,
timerData,
)
const displayedTime = `${
timerData.minutes >= 10 ? timerData.minutes : `0${timerData.minutes}`
}:${timerData.seconds >= 10 ? timerData.seconds : `0${timerData.seconds}`}`
const startTimer = () => {
setTimerData({ ...timerData, isOn: true })
}
const stopTimer = () => {
setTimerData({ ...timerData, isOn: false })
}
const resetTimer = () => {
setTimerData(initialState)
}
return (
<div className="timer__container" data-testid="timerContainer">
<div className="timer__time-display" data-testid="timeDisplayContainer">
{displayedTime}
</div>
<div className="timer__button-wrap">
<TimerButton buttonAction={startTimer} buttonValue="Start" />
<TimerButton buttonAction={stopTimer} buttonValue="Stop" />
<TimerButton buttonAction={resetTimer} buttonValue="Reset" />
</div>
</div>
)
}
export default Timer
And the test (there is a beforeEach and afterEach calling jest.useFakeTimers() and jest.useRealTimers() respectively above):
it("Assert timer counts down by correct interval when starting timer", async () => {
render(<Timer />)
const initialTime = screen.getByText("25:00")
const startTimerButton = screen.getByText("Start")
expect(initialTime).toBeInTheDocument() // This works fine
fireEvent.click(startTimerButton)
act(() => {
jest.advanceTimersByTime(5000)
})
await waitFor(() => expect(screen.getByText("24:55")).toBeInTheDocument())
})
Output of the test:
Unable to find an element with the text: 24:55. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="timer__container"
data-testid="timerContainer"
>
<div
class="timer__time-display"
data-testid="timeDisplayContainer"
>
24:59 <--- This value is always the same
</div>
<div
class="timer__button-wrap"
>
{ ... }
</div>
</div>
</body>
Id like to assert that the correct time is being displayed after moving the time forward by different increments, if I can do this then it will allow for a much more comprehensive set of tests.
I have tried using many of the timing methods in Jest including advanceTimersByTime(), runOnlyPendingTimers(), runAllTimers() to no avail, I can never get the time to advance by more than 1 second. I have also added console.log()s in at various points in the useInterval function and can verify that the function is being called a varying number of times depending on how much I try to advance the time by (it gets called numbers of seconds + 1 times as expected).
When logging out the state data I have seen that the timerData state does not update except for the last run through:
callback {"minutes":25,"seconds":0,"isOn":true} // Every time but the last
callback {"minutes":24,"seconds":59,"isOn":true} // The last time only
Im thinking it must be something to do with how Im referencing the state values during update, or how Jests fake timers interact with state updates though I really dont know at this point
So, after a lot of playing and trying many different methods I have a solution for this.
Further investigation into the calling of the useInterval() function called showed that:
the correct lines to update state were being reached, but the component was only re-rendering once after all these loops of the setInterval function despite the number of updates to state that were supposedly triggered (verified using console logs)
altering the time passed to setInterval (i) to be less than 500ms showed that the time was advancing by amounts roughly equivalent to 1 % i
the component only re-renders once for each tick of the setInterval function that occurs under the time passed to jest.advanceTimersByTime()
From this it appears that for each call of jest.advanceTimersByTime() only one iteration of the setInterval function can be moved through, and so the only way I found to move the interval forward by the correct amount of time is to call advanceTimersByTime() repeatedly via a helper function:
const advanceJestTimersByTime = (increment, iterations) => {
for (let i = 0; i < iterations; i++) {
act(() => {
jest.advanceTimersByTime(increment)
})
}
}
Test updated to:
it("Assert timer counts down by correct interval when starting timer", async () => {
render(<Timer />)
const initialTime = screen.getByText("25:00")
const startTimerButton = screen.getByText("Start")
expect(initialTime).toBeInTheDocument()
fireEvent.click(startTimerButton)
advanceJestTimersByTime(1000, 60)
await waitFor(() => expect(screen.getByText("24:00")).toBeInTheDocument())
})
This passes and allows for assertions to be made at any point for the countdown

How do I loop an image carousel with React setState and setInterval?

I am trying to set up an image carousel that loops through 3 images when you mouseover a div. I'm having trouble trying to figure out how to reset the loop after it reaches the third image. I need to reset the setInterval so it starts again and continuously loops through the images when you are hovering over the div. Then when you mouseout of the div, the loop needs to stop and reset to the initial state of 0. Here is the Code Sandbox:
https://codesandbox.io/s/pedantic-lake-wn3s7
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
let timer;
const [count, setCount] = useState(0);
const updateCount = () => {
timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
if (count === 3) clearInterval(timer);
};
const origCount = () => {
clearInterval(timer);
setCount((count) => 0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
Anything involving timers/intervals is an excellent candidate for useEffect, because we can easily register a clear action in the same place that we set the timer using effects with cleanup. This avoids the common pitfalls of forgetting to clear an interval, e.g. when the component unmounts, or losing track of interval handles. Try something like the following:
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [mousedOver, setMousedOver] = useState(false);
useEffect(() => {
// set an interval timer if we are currently moused over
if (mousedOver) {
const timer = setInterval(() => {
// cycle prevCount using mod instead of checking for hard-coded length
setCount((prevCount) => (prevCount + 1) % images.length);
}, 1000);
// automatically clear timer the next time this effect is fired or
// the component is unmounted
return () => clearInterval(timer);
} else {
// otherwise (not moused over), reset the counter
setCount(0);
}
// the dependency on mousedOver means that this effect is fired
// every time mousedOver changes
}, [mousedOver]);
return (
<div className="App">
<div className="title">Image Rotate</div>
<div
// just set mousedOver here instead of calling update/origCount
onMouseOver={() => setMousedOver(true)}
onMouseOut={() => setMousedOver(false)}
>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
As to why your code didn't work, a few things:
You meant to say if (count === 2) ..., not count === 3. Even better would be to use the length of the images array instead of hardcoding it
Moreover, the value of count was stale inside of the closure, i.e. after you updated it using setCount, the old value of count was still captured inside of updateCount. This is actually the reason to use functional state updates, which you did when you said e.g. setCount((prevCount) => prevCount + 1)
You would have needed to loop the count inside the interval, not clear the interval on mouse over. If you think through the logic of it carefully, this should hopefully be obvious
In general in react, using a function local variable like timer is not going to do what you expect. Always use state and effects, and in rarer cases (not this one), some of the other hooks like refs
I believe that setInterval does not work well with function components. Since callback accesses variables through closure, it's really easy to shoot own foot and either get timer callback referring to stale values or even have multiple intervals running concurrently. Not telling you cannot overcome that, but using setTimeout is much much much easier to use
useEffect(() => {
if(state === 3) return;
const timerId = setTimeout(() => setState(old => old + 1), 5000);
return () => clearTimeout(timerId);
}, [state]);
Maybe in this particular case cleanup(clearTimeout) is not required, but for example if user is able to switch images manually, we'd like to delay next auto-change.
The timer reference is reset each render cycle, store it in a React ref so it persists.
The initial count state is closed over in interval callback scope.
There are only 3 images so the last slide will be index 2, not 3. You should compare against the length of the array instead of hard coding it.
You can just compute the image index by taking the modulus of count state by the array length.
Code:
export default function App() {
const timerRef = useRef();
const [count, setCount] = useState(0);
// clear any running intervals when unmounting
useEffect(() => () => clearInterval(timerRef.current), []);
const updateCount = () => {
timerRef.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const origCount = () => {
clearInterval(timerRef.current);
setCount(0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img
src={images[count % images.length].source} // <-- computed index to cycle
alt={images.name}
/>
<p>count is: {count}</p>
</div>
</div>
);
}
Your setCount should use a condition to check to see if it should go back to the start:
setCount((prevCount) => prevCount === images.length - 1 ? 0 : prevCount + 1);
This will do setCount(0) if we're on the last image—otherwise, it will do setCount(prevCount + 1).
A faster (and potentially more readable) way of doing this would be:
setCount((prevCount) => (prevCount + 1) % images.length);

Why is the setInterval function not working properly?

It keeps showing the error message that it is an infinite loop. I am only beggining to learn React, and this is a Clicker game. How do I change my code to make the setInterval work. Thank you.(BTW I do not want any other changes to the code that won't affect the setInterval function. And yes, I have used setInterval in many projects already and it worked out fine.)
import "./styles.css";
export default function App() {
let [num, setNum] = useState(0);
let [add, setAdd] = useState(1);
let [numC, setNumC] = useState(0);
let [numCP, setNumCP] = useState(10);
let [numW, setNumW] = useState(0);
let [numWP, setNumWP] = useState(20)
setInterval(setNum(num+=numW),3000);
const click = () => {
setNum((num += add));
};
const clicker = () => {
if (num >= numCP) {
setNumC((numC += 1));
setNum((num -= numCP));
setNumCP((numCP += 5));
setAdd((add += 1));
}
};
const worker = () => {
if (num >= numWP) {
setNumW((numW += 1));
setNum((num -= numWP));
setNumWP((numWP += 10));
}
};
return (
<div className="App">
<h1>Clicker Game</h1>
<div>
{num}
<button onClick={click}>Click</button>
</div>
<p />
<div>
{numC}
<button onClick={clicker}>Buy({numCP})</button>
</div>
<div>
{numW}
<button onClick={worker}>Buy({numWP})</button>
</div>
</div>
);
}```
There are a couple of issues.
First you are immediately calling the setNum when you should be passing a callback to be executed when the interval is passed.
So setInterval(() => setNum(num+=numW),3000);
But now you have the second issue, each time the component is re-rendered you will initiate an additional interval. (and it will be re-rendered a lot, at the minimum each time the interval callback is fired)
So you would likely need to use a useEffect, with 0 dependencies so it runs once, if you want to set it and let it run continuously.
useEffect(() => {
setInterval(() => setNum(num += numW), 3000);
}, []);
But now you will encounter yet another issue. The num and numW used in the interval will be locked to the values in the first render of the component.
For the num you can work around it, by using the callback syntax of the setNum
useEffect(() => {
setInterval(() => setNum(currentNum => currentNum += numW), 3000);
}, []);
but numW will never update.
A final tool, is to reset the interval each time the numW or num changes. To do that you will need to return a function from the useEffect that does the clearing.
useEffect(() => {
const interval = setInterval(() => setNum(currentNum => currentNum += numW), 3000);
return () => clearInterval(interval);
}, [numW]);
But this will have the minor issue that the interval is now not constant, since it resets.
Every time one of your state variables changes, the component is re-rendered, i.e. the function App is called again. This will call setInterval over and over again.
You want to look at useEffect to put your setIntervals in.

React Incremeting Counter Behind

I've just learned to use UseState to create a simple incremental counter, but I've noticed some odd behavior with the count being 2 numbers behind (+ - ) in console.log. Now on the screen the number displays fine, but this creates an issue because I'm trying to change the color of the number if it's negative or positive.
Because I'm trying to change the display color of the number on the screen, would UseEffect be a good solution to this problem? I'm going to go back and watch some YT videos on UseEffect, but figured I'd ask here as well. I was thrilled when I was able to figure out how to change the classnames using state, but then got a pie in the face when the numbers weren't changing colors correctly.
Here's an example of the behavior I'm seeing.
const { useState } = React
function Vote () {
const [count, setCount] = useState(0)
const [color, setColor] = useState('black')
function handleDecrement () {
setCount(count - 1)
checkCount()
}
function handleIncrement () {
setCount(count + 1)
checkCount();
}
function checkCount () {
// Less than 0 make it red
if (count < 0) {
setColor('red')
console.log(count)
// Greater than 1 make it green
} else if (count > 0 ) {
setColor('green')
console.log(count)
// If it's 0 just keep it black
} else {
setColor('black')
console.log(count)
}
};
return (
<div>
<button onClick={handleDecrement}>-</button>
<h1 className={color}>{count}</h1>
<button onClick={handleIncrement}>+</button>
</div>
)
}
ReactDOM.render(<Vote />, document.getElementById('root'))
Yes, you can simply use an effect hook with dependency to check the color. When count updates the effect hook callback is triggered.
The issue is that react state updates are asynchronous, so the updated state count won't be available until the next render cycle; you are simply using the count value from the current render cycle.
Note: When incrementing/decrementing counts you should use a functional state update. This ensures state is correctly updated from the previous state in the case multiple state updates are enqueued within any single render cycle.
function Vote() {
const [count, setCount] = useState(0);
const [color, setColor] = useState("black");
function handleDecrement() {
setCount(count => count - 1);
}
function handleIncrement() {
setCount(count => count + 1);
}
useEffect(checkCount, [count]);
function checkCount() {
// Less than 0 make it red
if (count < 0) {
setColor("red");
console.log(count);
// Greater than 1 make it green
} else if (count > 0) {
setColor("green");
console.log(count);
// If it's 0 just keep it black
} else {
setColor("black");
console.log(count);
}
}
return (
<div>
<button onClick={handleDecrement}>-</button>
<h1 className={color}>{count}</h1>
<button onClick={handleIncrement}>+</button>
</div>
);
}
When updating the state based on the current state always use the callback version of setState which receives the current state as an argument and should return the next state. React batches state updates and relying on what has been returned by useState to update can yield incorrect results. Also the way to check for a change to count and update accordingly is by using useEffect with count as a dependency. The console.log() in your example will still log the old state as state updates are async and can only be seen during the next render.
const [count, setCount] = useState(0)
const [color, setColor] = useState('black')
function handleDecrement () {
setCount(current => current - 1);
}
function handleIncrement () {
setCount(current => current + 1)
}
useEffect(() => {
// Less than 0 make it red
if (count < 0) {
setColor('red')
console.log(count)
// Greater than 1 make it green
} else if (count > 0 ) {
setColor('green')
console.log(count)
// If it's 0 just keep it black
} else {
setColor('black')
console.log(count)
}
}, [count]);

React Function Component counter with hooks

I'm trying to understand the new React hooks and their use cases.
My goal is a single component that counts up and also every x tick counts another counter.
I have achieved it using useEffect and useState, with two main problems:
1. A memory leak when the component unmounts before the timeout gets called (when navigating using react-router)
2. The component renders twice on every tick because useEffect and useState both trigger the render.
I think the solution will be something with useRef or useMemo but I haven't figured it out yet.
My current component (with typescript):
import React from "react";
const Component: React.FC = () => {
const [trigger, setTrigger] = React.useState(0);
const [timer, setTimer] = React.useState({ cycle: 0, count: 0 });
let refTimer = React.useRef({ cycle: 0, count: 0 });
// useRef
// React.useEffect(() => {
// setInterval(() => {
// console.log("tick");
// if (refTimer.current.count % 2 === 0) {
// refTimer.current.cycle++;
// setTimer(refTimer.current);
// }
// refTimer.current.count++;
// setTimer(refTimer.current);
// // console.log(timer);
// }, 1000);
// }, []);
// useState
React.useEffect(() => {
console.log("effect tick");
setTimeout(() => {
console.log("tick");
const count = timer.count + 1;
if (count % 2 === 0) {
const cycle = timer.cycle + 1;
setTimer({ ...timer, count, cycle });
return;
}
setTimer({ ...timer, count });
}, 1000);
}, [timer]);
return (
<div>
<br />
<br />
<br />
<br /> Playground:
<div>Count: {timer.count}</div>
<div>Cycle: {timer.cycle}</div>
<button type="button" onClick={(): void => setTrigger(trigger + 1)}>
Trigger Count: {trigger}
</button>
</div>
);
};
export default Component;
As I said, like this I have the mentioned two problems. I can remove the useEffect entirely that would fix the double render but when I click the Trigger Button the ticks will stack up which is worse than double renders.
The commented useRef part is what I have tried but it somehow doesn't work.
I appreciate all the help!
Edit:
A third minor problem is that like this the counter runs only with setTimeout which will trigger another setTimeout, so if that process takes some time it won't really be an exact interval.
So my goal is an interval that runs in a separate process (I'd say inside a useEffect) what will cause a rerender on every tick and won't stack up on each call or when something else triggers a rerender.
You can fix the memory leak mentioned in #1.
React.useEffect(() => {
console.log("effect tick", timer);
// .. 👇 get the timeout ID to clear on unmount
const id = setTimeout(() => {
console.log(`tick id=${id}`, timer);
const count = timer.count + 1;
if (count % 2 === 0) {
const cycle = timer.cycle + 1;
setTimer({ ...timer, count, cycle });
return;
}
setTimer({ ...timer, count });
}, 1000);
// ... 👇 Clean up here with the ID on unmount
return () => clearTimeout(id);
}, [timer]);
Regarding #2 double render, would you be more specific?
Before/After cleaning up in useEffect above, I am unable to figure out what you mean as the current console logs seem to work as expected.

Resources