How to proper setup setTimeout with recursively using React hooks? - reactjs

I'm building a component that should show elements of an array, one per time and it shows the element for a period of time determined by the data. With the data below I would like to show textA for 5 seconds and then change to textB which I would show for 20 seconds, and then show textC for 10 seconds and start over again, showing textA and so on.
Here is what I'm trying:
const slides = [
{text: 'textA, time_length: 5},
{text: 'textB', time_length: 20},
{text: 'textC', time_length: 10}
] ;
const play = () => {
if (slides && slides.length > 0) {
const playSlides = () => {
this.timer = setTimeout(() => {
// update index
if (currentIndex + 1 < splashes.length) {
setCurrentIndex(currentIndex + 1)
} else {
setCurrentIndex(0)
}
}, slides[currentIndex])
}
}
}
useEffect(() => {
if (slides.length) {
debugger
play(slides)
}
return clearTimeout(this.timer)
}, [slides])
return <p>{slides[currentIndex]}</p>
}

This solution will show the slide.text every slide.time_length.
Notice that the 'loop' is achieved via the dependency [currentIndex]; in other words, every time the currentIndex changes, the useEffect will run once more, and will update the currentIndex after the next time_length seconds.
The calculation (currentIndex + 1) % slides.length will reset the index back to 0 if it overflows the array length (it's just a shortened version of your conditional, nothing special).
const slides = [
{ text: 'textA', time_length: 5 },
{ text: 'textB', time_length: 20 },
{ text: 'textC', time_length: 10 }
];
const TextSlider = () => {
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (slides.length) {
const timeoutId = setTimeout(() => {
setCurrentIndex((currentIndex + 1) % slides.length)
}, slides[currentIndex].time_length * 1000)
return () => clearTimeout(timeoutId);
}
return () => { }
}, [currentIndex])
return <h1>{slides[currentIndex].text}</h1>
}
Sometimes being less wordy can make it easier to see what's going on. Your mileage may vary -
import { useState, useEffect } from "react"
function TextSlider ({ slides = [] }) {
const [i, nextSlide] =
useState(0)
useEffect(() => {
if (i >= slides.length) return
const t =
setTimeout
( nextSlide
, slides[i].time_length * 1000
, (i + 1) % slides.length
)
return () => clearTimeout(t)
}, [i])
return <h1>{slides[i].text}</h1>
}
ReactDom.render(document.body, <TextSlider slides={slides} />)

Related

Not clear why the use effect hook gets triggered too many times

I am trying to build a timer with react
for some sort of reason, it is not incrementing properly - the use effect hook gets triggered too many times and I do know why is that happening
instead of incrementing in 1 second intervals it in increments 3 or more second intervals
Maybe you explain why the hook gets triggered so many times and what i could do to resolve the issue
const Timer = () => {
const [timeDisplayed, setTimeDisplayed] = useState(0);
const [startTime, setStartTime] = useState(0)
const [timerOn, setTimerOn] = useState(false);
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
const start = () => {
setStartTime(Date.now())
setTimerOn(true)
}
const stop = () => {
setTimerOn(false)
}
const reset = () => {
setTimerOn(false)
setTimeDisplayed(0)
}
return (
<div >
<h2>Timer</h2>
<p>{timeDisplayed}</p>
<div id="buttons">
<button disabled={timerOn} onClick={() => {start()}}>{timeDisplayed===0 ? 'Start' : 'Resume'}</button>
<button disabled={!timerOn} onClick={() => stop()}>Stop</button>
<button disabled={timeDisplayed === 0} onClick={() => reset()}>Reset</button>
</div>
</div>
);
};
export default Timer;```
When you set timerOn you start updating timeDisplayed every second, and because your effect specifies timeDisplayed as a dependency it runs again. timerOn is still true, so it calls setInterval again, and now you have it updating twice every second. Then three times. Four. And so on.
You could fix this by either 1) returning a cleanup function that clears the interval, or 2) setting interval back to null when the timer is off and adding it to your start condition:
if (timerOn && !interval) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (interval) {
clearInterval(interval);
interval = null;
}
}, [timerOn, timeDisplayed]);
You useEffect is updating state that it is dependent on. Meaning it calls itself recursively.
Make use of the callback function of your state setter setTimeDisplayed, and remove timeDisplayed from your dependency array.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
Here issue is not with the useEffect hook. It's in the logic you are using to update value of timeDisplayed. You are adding previous value with new updated count.
For example :
when timeDisplayed = 1, Math.floor(delta / 1000) returns 2. That's why on next update timeDisplayed'svalue is set to 3 (timeDisplayed + Math.floor(delta / 1000)) instead of 2.
Try adding a console.log statement as shown below to see all values at each update.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
console.log('vals',timeDisplayed, Math.floor(delta / 1000),timeDisplayed + Math.floor(delta / 1000) )
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
Update you useEffect to this and your problem should be solved.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);

useState with an argument in it's array is breaking a setInterval and makes it glitchy and erratic

I just asked a question earlier here: react value of a state variable different in a different function
and now I have a new problem.
having a useEffect that looks like this
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
is breaking a setInterval that looks like this:
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
The full component looks like this:
import React, { useEffect, useState } from 'react';
function ScoreCard() {
const [strokeScore, setStrokeScore] = useState(1);
const [totalStrokeScore, setTotalStrokeScore] = useState(1);
const [strokeCountdown, setStrokeCountdown] = useState();
const strokeCountdownDing = new Audio('/sounds/round-complete.mp3');
// make new variable, maybe?
let strokeScoreCount = 0;
const endOfGameRound = () => {
strokeCountdownDing.play();
document.getElementById('stroke-counter-button').disabled = true;
}
const addToStrokeScore = () => {
setStrokeScore(prev => prev + 1);
// prints the correct number
console.log('Score in function', strokeScore);
if (strokeCountdown === 0) {
endOfGameRound()
}
}
const subtractStrokeScore = () => {
setStrokeScore(strokeScore - 1);
}
const countDown = () => {
// let strokeCountdown = Math.floor(Math.random() * 31) + 100;
let strokeCountdown = 20
let strokeCountdownSpeedOptions = [1000, 500, 300, 200];
let strokeCountDownSpeed = strokeCountdownSpeedOptions[Math.floor(Math.random()*strokeCountdownSpeedOptions.length)];
let strokeCounter = setInterval(() => {
strokeCountdown--
setStrokeCountdown(strokeCountdown)
if (strokeCountdown === 0) {
endOfGameRound()
clearInterval(strokeCounter)
setTotalStrokeScore(strokeScore);
}
}, strokeCountDownSpeed)
}
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
return (
<div className="game__score-card">
<div className="game__speed-level">
Speed: idk
</div>
<div className="game__stroke-countdown">
Countdown: {strokeCountdown}
</div>
<p>Score: {strokeScore}</p>
<button id="stroke-counter-button" onClick={addToStrokeScore}>
{strokeCountdown === 0 ? 'Game Over' : 'Stroke'}
</button>
{/* window.location just temp for now */}
{strokeCountdown === 0
? <button onClick={() => window.location.reload(false)}>Play Again</button>
: <button disabled>Game in Progress</button>
}
<div className="game__total-score">
Total score: {totalStrokeScore}
</div>
</div>
);
}
export default ScoreCard;
When I click on the button, the timer gets erratic and goes all over the place.
All I want to do is make it so that the timer counts down smoothly, gets the clicks the user made and add it to total score.
Why is
useEffect(() => {
countDown();
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);
Breaking everything?
I was calling countDown() everytime I clicked so I just did
useEffect(() => {
if (strokeScore === 1) {
countDown();
}
console.log('Score in useeffect', strokeScore);
}, [strokeScore]);

Countdown timer with react hooks

I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
import React, { useState } from 'react';
const Countdown = ({ countDownTimerOpt }) => {
const [getObj, setObj] = useState({
formatTimer: null,
countDownTimer: 0,
intervalObj: null,
});
const { formatTimer, countDownTimer, intervalObj } = getObj;
if (countDownTimerOpt > 0 && intervalObj === null) {
startTimer();
}
function startTimer() {
const x = setInterval(() => {
timer();
}, 1000);
setObj((prev) => ({
...prev,
countDownTimer: countDownTimerOpt,
intervalObj: x,
}));
}
function timer() {
var days = Math.floor(countDownTimer / 24 / 60 / 60);
var hoursLeft = Math.floor(countDownTimer - days * 86400);
var hours = Math.floor(hoursLeft / 3600);
var minutesLeft = Math.floor(hoursLeft - hours * 3600);
var minutes = Math.floor(minutesLeft / 60);
var remainingSeconds = countDownTimer % 60;
const formatTimer1 =
pad(days) +
':' +
pad(hours) +
':' +
pad(minutes) +
':' +
pad(remainingSeconds);
if (countDownTimer === 0) {
clearInterval(intervalObj);
} else {
setObj((prev) => ({
...prev,
formatTimer: formatTimer1,
countDownTimer: prev['countDownTimer'] - 1,
}));
}
}
function pad(n) {
return n < 10 ? '0' + n : n;
}
return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;
import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';
const Training = () => {
const [getValue, setValue] = useState(0);
useEffect(() => {
const x = setTimeout(() => {
console.log('setTimeout');
setValue(10000);
}, 5000);
return () => clearInterval(x);
}, []);
return <Timer countDownTimerOpt={getValue} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
const useTimer = (startTime) => {
const [time, setTime] = useState(startTime)
const [intervalID, setIntervalID] = useState(null)
const hasTimerEnded = time <= 0
const isTimerRunning = intervalID != null
const update = () => {
setTime(time => time - 1)
}
const startTimer = () => {
if (!hasTimerEnded && !isTimerRunning) {
setIntervalID(setInterval(update, 1000))
}
}
const stopTimer = () => {
clearInterval(intervalID)
setIntervalID(null)
}
// clear interval when the timer ends
useEffect(() => {
if (hasTimerEnded) {
clearInterval(intervalID)
setIntervalID(null)
}
}, [hasTimerEnded])
// clear interval when component unmounts
useEffect(() => () => {
clearInterval(intervalID)
}, [])
return {
time,
startTimer,
stopTimer,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
import { useEffect, useRef, useState } from 'react';
export const useCountDown: (
total: number,
ms?: number,
) => [number, () => void, () => void, () => void] = (
total: number,
ms: number = 1000,
) => {
const [counter, setCountDown] = useState(total);
const [startCountDown, setStartCountDown] = useState(false);
// Store the created interval
const intervalId = useRef<number>();
const start: () => void = () => setStartCountDown(true);
const pause: () => void = () => setStartCountDown(false);
const reset: () => void = () => {
clearInterval(intervalId.current);
setStartCountDown(false);
setCountDown(total);
};
useEffect(() => {
intervalId.current = setInterval(() => {
startCountDown && counter > 0 && setCountDown(counter => counter - 1);
}, ms);
// Clear interval when count to zero
if (counter === 0) clearInterval(intervalId.current);
// Clear interval when unmount
return () => clearInterval(intervalId.current);
}, [startCountDown, counter, ms]);
return [counter, start, pause, reset];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv

Update state in useEffect shows warning

I want to update a state after some other state is updated:
export default function App() {
const [diceNumber, setDiceNumber] = useState(0);
const [rolledValues, setRolledValues] = useState([
{ id: 1, total: 0 },
{ id: 2, total: 0 },
{ id: 3, total: 0 },
{ id: 4, total: 0 },
{ id: 5, total: 0 },
{ id: 6, total: 0 }
]);
const rollDice = async () => {
await startRolingSequence();
};
const startRolingSequence = () => {
return new Promise(resolve => {
for (let i = 0; i < 2500; i++) {
setTimeout(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
}, (i *= 1.1));
}
setTimeout(resolve, 2600);
});
};
useEffect(()=>{
if(!diceNumber) return;
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const newValue = rolledValues[valueIdx];
const {total} = newValue;
newValue.total = total + 1;
setRolledValues([
...rolledValues.slice(0,valueIdx),
newValue,
...rolledValues.slice(valueIdx+1)
])
}, [diceNumber]);
return (
<div className="App">
<button onClick={rollDice}>Roll the dice</button>
<div> Dice Number: {diceNumber ? diceNumber : ''}</div>
</div>
);
}
Here's a sandbox
When the user rolls the dice, a couple setTimeouts will change the state value and resolve eventually. Once it resolves I want to keep track of the score in an array of objects.
So when I write it like this, it works but eslint gives me a warning of a missing dependency. But when I put the dependency in, useEffect will end in a forever loop.
How do I achieve a state update after a state update without causing a forever loop?
Here's a way to set it up that keeps the use effect and doesn't have a dependency issue in the effect: https://codesandbox.io/s/priceless-keldysh-wf8cp?file=/src/App.js
This change in the logic of the setRolledValues call includes getting rid of an accidental mutation to the rolledValues array which could potentially cause issues if you were using it in other places as all React state should be worked with immutably in order to prevent issues.
The setRolledValues has been changed to use the state callback option to prevent a dependency requirement.
useEffect(() => {
if (!diceNumber) return;
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, [diceNumber]);
I wouldn't recommend working with it like this, though as it has an issue where if the same number is rolled multiple times in a row, the effect only triggers the first time.
You can move the logic into the rollDice callback instead, which will get rid of both issues that was occurring. https://codesandbox.io/s/stoic-visvesvaraya-402o1?file=/src/App.js
I added a useCallback around rollDice to ensure it doesn't change references so it can be used within useEffects.
const rollDice = useCallback(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === num);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
// newValue,
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, []);
You should be able to just stick all the logic within setRolledValues
useEffect(() => {
if (!diceNumber) return;
setRolledValues((prev) => {
const valueIdx = prev.findIndex(val => val.id === diceNumber);
const newValue = prev[valueIdx];
const { total } = newValue;
newValue.total = total + 1;
return [
...prev.slice(0, valueIdx),
newValue,
...prev.slice(valueIdx + 1)
]
})
}, [diceNumber]);
EDIT: As others have pointed out, useEffect for this application appears to be ill-suited, as you could simply update setRolledValues in your function instead.
If there is some sort of underlying system we're not being shown where you must use an observer pattern like this, you can change the datatype of diceNumber to an object instead, that way subsequent calls to the same number would trigger useEffect

React app freezing when setInterval is called

I am trying to implement Conway's Game of Life in React, but it is freezing whenever a new generation is called. I assume this is because there is too much overhead caused by constantly re-rendering the DOM, but I don't know how to resolve this, nor can I think of an alternative to simply posting my entire code, so I apologise in advance for the verbosity.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import styled from "styled-components"
interface TileProps {
bool: boolean
}
const Tile: React.FC<TileProps> = ({bool}) => {
const colour = bool == true ? "#00FF7F" : "#D3D3D3"
return (
<div style = {{backgroundColor: colour}}/>
)
}
interface GridProps {
cells: boolean[][]
}
const StyledGrid = styled.div`
display: grid;
grid-template-columns: repeat(100, 1%);
height: 60vh;
width: 60vw;
margin: auto;
position: relative;
background-color: #E182A8;
`
const Grid: React.FC<GridProps> = ({cells}) => {
return (
<StyledGrid>
{cells.map(row => row.map(el => <Tile bool = {el}/>))}
</StyledGrid>
)
}
const randomBoolean = (): boolean => {
const states = [true, false];
return states[Math.floor(Math.random() * states.length)]
}
const constructCells = (rows: number, columns: number): boolean[][] => {
return constructEmptyMatrix(rows, columns).map(row => row.map(e => randomBoolean()))
}
const constructEmptyMatrix = (rows: number, columns: number): number[][] => {
return [...Array(rows)].fill(0).map(() => [...Array(columns)].fill(0));
}
const App: React.FC = () => {
const columns = 100;
const rows = 100;
const [cells, updateCells] = useState<boolean[][]>(constructCells(rows, columns));
useEffect(() => {
const interval = setInterval(() => {
newGeneration();
}, 1000);
return () => clearInterval(interval);
}, []);
const isRowInGrid = (i: number): boolean => 0 <= i && i <= rows - 1
const isColInGrid = (j : number): boolean => 0 <= j && j <= columns -1
const isCellInGrid = (i: number, j: number): boolean => {
return isRowInGrid(i) && isColInGrid(j)
}
const numberOfLiveCellNeighbours = (i: number, j: number): number => {
const neighbours = [
[i - 1, j], [i, j + 1], [i - 1, j + 1], [i - 1, j + 1],
[i + 1, j], [i, j - 1], [i + 1, j - 1], [i + 1, j + 1]
]
const neighboursInGrid = neighbours.filter(neighbour => isCellInGrid(neighbour[0], neighbour[1]))
const liveNeighbours = neighboursInGrid.filter(x => {
const i = x[0]
const j = x[1]
return cells[i][j] == true
})
return liveNeighbours.length;
}
const updateCellAtIndex = (i: number, j: number, bool: boolean) => {
updateCells(oldCells => {
oldCells = [...oldCells]
oldCells[i][j] = bool;
return oldCells;
})
}
const newGeneration = (): void => {
cells.map((row, i) => row.map((_, j) => {
const neighbours = numberOfLiveCellNeighbours(i, j);
if (cells[i][j] == true){
if (neighbours < 2){
updateCellAtIndex(i, j, false);
} else if (neighbours <= 3){
updateCellAtIndex(i, j, true);
}
else {
updateCellAtIndex(i, j, false);
}
} else {
if (neighbours === 3){
updateCellAtIndex(i, j, true);
}
}
}))
}
return (
<div>
<Grid cells = {cells}/>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
The application freezes because React does not batch your individual state updates. More information about this can be found in this answer
You have two options here.
Use ReactDOM.unstable_batchedUpdates:
This can be done with a single line change, but note that the method is not part of the public API
useEffect(() => {
const interval = setInterval(() => {
// wrap generation function into batched updates
ReactDOM.unstable_batchedUpdates(() => newGeneration())
}, 1000);
return () => clearInterval(interval);
}, []);
Update all states in one operation.
You could refactor your code to set updated cells only once. This option does not use any unstable methods
useEffect(() => {
const interval = setInterval(() => {
// `newGeneration` function needs to be refactored to remove all `updateCells` calls. It should update the input array and return the result
const newCells = newGeneration(oldCells);
// there will be only one call to React on each interval
updateCells(newCells);
}, 1000);
return () => clearInterval(interval);
}, []);

Resources