set interval in react repeating itself - reactjs

I am trying to make a typing animation for my portfolio and I have an array of words i want it to type and i tried to make it so i can just update the word every 5 seconds or so. So i made a set interval that will update a useState that updates the word that will be displayed
const [displayTyped, setDisplayTyped] = useState("Developer");
const typedWords = ['Developer', 'Designer', 'Freelancer', 'Photographer'];
let currentWord = 0;
setInterval(() => {
setDisplayTyped(typedWords[currentWord])
if(currentWord < 3) {
currentWord++;
} else {
currentWord = 0;
};
}, 5000);
but when i do that it updates twice and stays at like developer and then the longer i wait it goes through all of them like 6 times and changes it instantly all at once and i don't know who its doing that
and when i console.log(currentWord) it shows that it happens two times so I'm thinking that the useState is re-loading the page and its setting the word to the default value

Every time you render, you create a new setInterval, and currently have no mechanism of clearing it. What I would probably do is
useEffect(() => {
let currentWord = 0;
const interval = setInterval(() => {
setDisplayTyped(typedWords[currentWord]);
currentWord = ++currentWord % 4;
}, 5000);
return () => clearInterval(interval);
}, []);

Try this:
const [displayTyped, setDisplayTyped] = useState("Developer");
const typedWords = ['Developer', 'Designer', 'Freelancer', 'Photographer']; // <- move this outside of the component if it's just static.
const [currentWord, setCurrentWord] = useState(0);
useEffect( () => {
function updateWord() {
setDisplayTyped(typedWords[currentWord])
if(currentWord < 3) {
setCurrentWord(currentWord + 1);
} else {
setCurrentWord(0);
};
}
setTimeout(updateWord, 5000);
}, [currentWord])
Since React will render this component with every state update, you don't need to use setInterval. The 5 second setTimeout call will be called with each render. currentWord will update and so the useEffect will run and setDisplayTyped will be called.
Here is a sandbox link to show the effect:
codesandbox.io

Related

How to use useEffect/state/variables properly without user interaction?

My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once

How to update state using setInterval on functional components in React

I am trying to implement a countdown, but the state is not being update as expected. It stays stuck on initial value 30. I have no clue how to solve it. Can anyone help me please?
const [timer, setTimer] = useState(30);
function handleTimer() {
const interval = setInterval(() => {
setTimer((count) => count - 1);
if (timer <= 0) {
clearInterval(interval);
}
}, 1000);
}
useEffect(() => {
handleTimer();
}, []);
The problem is about javascript closures, you can read more about it here
Also, Dan has a full detailed article talking about this specific problem. I strongly suggest you read it.
And here is a quick solution and demonstration for your problem. First of all, the useEffect will be executed every time the component is remount. And this could happen in many different scenarios depending on your code. Hence, The useEffect starts fresh and closes on new data every time.
So all we need is to save our values into ref so we can make use of the same reference every re-render.
// Global Varibales
const INITIAL_TIMER = 30;
const TARGET_TIMER = 0;
// Code refactoring
const [timer, setTimer] = useState(INITIAL_TIMER);
const interval = useRef();
useEffect(() => {
function handleTimer() {
interval.current = setInterval(() => {
setTimer((count) => count - 1);
}, 1000);
}
if (timer <= TARGET_TIMER && interval.current) {
clearInterval(interval.current);
}
if (timer === INITIAL_TIMER) {
handleTimer();
}
}, [timer]);
You can find here a more generic hook to handle the setInterval efficiently in react with pause and limit the number of iterations:
https://github.com/oulfr/react-interval-hook

how to unmount a function in useEffect (react js)

test function dose not unmount and wen i click on correectAnswer the last function (test) is steal running and again test function will run and then when the last test function achieve to 0 we go to loser page.
const [state, setState] = useState({
haveTime: 10
})
const [states] = useState({
correct: "question",
step: "loser"
})
const test = (timer) => {
let haveTime = 10
let time = setInterval(() => {
haveTime -= 1;
setState({ haveTime })
// console.log(state.haveTime)
}, 1000);
setTimeout(() => {
clearInterval(time)
dispatch(getNameStep(states.step))
}, timer);
}
const correectAnswer = () => {
if (index === 9) {
dispatch(getNameStep(stateForWinner.step))
}
else {
dispatch({
type: "indexIncrease"
})
test(10000)
}
}
let { question, correct_answer } = details.question[index];
useEffect(() => {
test(10000)
}, [])
There are a few things wrong with your code.
First you are combining setInterval with setTimeout which is not a good idea just because of the amount of coordination that needs to happen.
Second to clear an interval or a timeout you need to do it from within the useEffect by returning a function.
Third you have no "dependencies" in your useEffect.
Look at this code that use in one my apps:
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
SetSearchFilter(SearchLocal, State.Reversed);
}, 250)
// this is how you clear a timeout from within a use effect
// by returning a function that does the disposing
return () => clearTimeout(delayDebounceFn);
}, [SearchLocal]);//here you need to add the actual dependencies of your useEffect
Lastly you need to breakdown your useEffect to perform a "single effect". Combining "too much stuff" into a single use effect is not good because then it is very difficult to debug and to achieve what you want.
You need to break down your useEffect into smaller useEffects.
You need to tell the useEffect when you want it to run by adding the dependencies. This way you know that a particular useEffect will run for ecxample if the "nextStep" has changed or if the test has reached the end.

useEffect hook using outdated variables in child functions

I am trying to use the useEffect hook as a way to create an async timer in react. The logic is inside of timeFunc(), and the useEffect is working such that it calls the function every 1000ms. The weird part is, for some reason when timeFunc() gets called (every one sec) it's only accesses the old variable values, (specifically "paused"). For example, if the interval starts with a value of "paused" being false, even if I change 'paused' to be true (paused is a state variable passed in by the parent component), timeFunc() will still think paused is false. Can't figure it out. Any help appreciated!
Code:
//TIMER MANAGER
let timeFunc = () => {
if(paused == false){
let delta = Math.trunc((new Date() - resumedTime)/1000);
setProgress(delta);
console.log('test + ' + paused);
} else {
clearInterval(interval);
}
}
useEffect(() => {
let interval = null;
interval = setInterval(() => {
timeFunc();
}, 1000);
return () => clearInterval(interval);
}, [initialized]);
The timeFunc depends on having an up-to-date value of paused, but it doesn't exist in the useEffect's dependency array.
Either add it to the dependency array and also store the time until the next interval in state, or use a ref for paused instead (with a stable reference) (or in addition to state), eg:
const pausedRef = useRef(false);
// ...
const timeFunc = () => {
if (!pausedRef.current) {
// ...
// to change it:
pausedRef.current = !pausedRef.current;
Also note that
let interval = null;
interval = setInterval(() => {
timeFunc();
}, 1000);
simplifies to
const interval = setInterval(timeFunc, 1000);

useEffect renders more than once even with empty dependency list

Here is my useEffect code block:
useEffect(() => {
const checkNextEmailTime = async () => {
const result = await agent.ContactCustomer.nextEmailTime();
const now = new Date();
const change = new Date(result);
if(change > now) {
setNextEmail(dateFormat(result, 'h:MM TT'));
setEmailPendingWarning(true);
} else {
setNextEmail('');
setEmailPendingWarning(false);
}
setTimeout(checkNextEmailTime, 60000);
}
checkNextEmailTime();
}, [])
I would expect to see this only ping my server every 60 seconds, yet what I am seeing is it will make a request from the server, wait 60 seconds, request again, then about 10 seconds later I see another request come in. This then repeats every 60 seconds.
As far as I know, this functional component only gets loaded one time.
Any ideas?
Thanks!
try to create a new function and remove setTimeout to the function, add useState
const [reload,setReload] = useState(false)
call this function and make it change the state setReload(!reload) and make your useEffect dependecy is reload, so now every 60 seconds state shloud change and call useEffect
const [reload,setReload] = useState(false)
useEffect(()=>{
checkNextEmailTime();
},[reload])
const checkNextEmailTime = ()=>{
// your code
...
setReload(!reload)
setTimeout(checkNextEmailTime, 60000);
}
I hope it helps
Following the trail #Ahmed left for me, I did some tweaking and came to this as the solution:
const checkNextEmailTime = useCallback(async () => {
const result = await agent.ContactCustomer.nextEmailTime();
const now = new Date();
const change = new Date(result);
if(change > now) {
setNextEmail(dateFormat(result, 'h:MM TT'));
setEmailPendingWarning(true);
} else {
setNextEmail('');
setEmailPendingWarning(false);
}
setTimeout(checkNextEmailTime, 60000);
}, []);
useEffect(() => {
checkNextEmailTime();
}, [checkNextEmailTime])
I didn't need to create a reload useState variable. Thanks for the help!

Resources