State not update when using setter and setInterval in useEffect - reactjs

I have the following code, which I expect it to print an extra 10 every 2 seconds on the screen. However, it only prints 10 once. console.log(digits) gave me the correct array of 10s. How can I add a 10 to the array every 2 seconds and print the updated array on the screen each time the 10 is added?
Code sandbox here: https://codesandbox.io/s/silly-https-zk58p?file=/src/App.js
import { useEffect, useState } from "react";
let data = [];
export default function App() {
const [digits, setDigits] = useState([]);
useEffect(() => {
let timer = setInterval(() => {
data.push(10);
setDigits(data);
console.log(digits);
}, 2000);
return () => clearInterval(timer);
}, [digits]);
return <div className="App">{digits}</div>;
}

The issue is with setDigits(data). With arrays, you should be executing setDigits([...data]).
Another way would be doing this:
let timer = setInterval(() => {
setDigits([...digits, 10]);
}, 2000);
Whenever dealing with objects, you should treat them as immutables. What happened here is you modifying an array and puhsing the SAME array into a state. While the value might be different, the array is actually the same hence it does not update the render. Whenever doing [...data] it creates a new array with the same values hence trigger the update.
useEffect picks up the new value change hence why it fires again(cant be observed by console.log()), but this does not trigger re-render of the component.

In your code, you are mutating the same array data by pushing 10 after each 2 seconds. As a result, the useEffect is not executing in the subsequent renders. So either you spread data array as in the following code snippet or simply get rid of the data variable and rely on digits array as Lith suggested.
export default function App() {
const [digits, setDigits] = useState([]);
useEffect(() => {
let timer = setInterval(() => {
data = [...data, 10];
setDigits(data);
console.log(digits);
}, 2000);
return () => clearInterval(timer);
}, [digits]);
return <div className="App">{digits}</div>;
}

Related

How to replace one component to another in React native

I have three component in screen with same size but different static data.
I want to replace the every 5 seconds on the screen in the same area(display position is fix). For ex. I have <FeedbackComp1>, <FeedbackComp2> and <FeedbackComp3>. So, I want display and replace the component every 5 seconds.
Display: first time render the screen display the <FeedbackComp1> and then replace the place <FeedbackComp2> and then <FeedbackComp3> and then <FeedbackComp1> so on.
If possible this functionality in array map() function so I also ready for that.
Thank you.
Something like this should do the trick.
import { useEffect, useState } from 'react'
// placeholders for your actual components.
const FeedbackComp1 = () => <></>
const FeedbackComp2 = () => <></>
const FeedbackComp3 = () => <></>
const MyComponentSwitcher = () => {
const [index, setIndex] = useState(0)
const components = [<FeedbackComp1/>, <FeedbackComp2/>, <FeedbackComp3/>]
useEffect(() => {
const interval = setInterval(() => {
setIndex(currentIndex => (currentIndex + 1) % components.length)
}, 5000)
return () => clearInterval(interval)
}, [])
return components[index]
}
Short explanation:
The index state-variable holds the index we're currently at.
The components array holds the components you want to switch between.
The useEffect initializes an interval of 5000 ms, so that every 5 seconds, it will set the index to one more than it currently is, using the remainder operator % to ensure we never have an index outside the array.
Note that the setIndex doesn't take in a new value, but rather a callback. This allows us to automatically set the value to one higher than before, without having to reference the value when the component is initialized. React reference.
The return statement simply returns the current component.

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

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

Change text at every time interval - React

I am trying to change a text (it begins automatically when the screen appears) at every time interval in react, but the problem is that, the time given isn't respected, and the text is changing at a random time interval.
This is a part of my code:
const names = [
'tony', 'elias', 'fadi'
]
const [newName, setnewName] = useState(0);
useEffect(() => {
for (const [index, value] of names.entries()) {
setTimeout(() => { shuffle(value) }, 5000);
}
})
const shuffle = (value) => {
setnewName(value);
}
And thank you!
Couple things here, but the main issue is the use of setTimeout in a useEffect call with no dependency array. So you're calling shuffle 5000ms after each render, which is why the updates seem to occur at random times. Additionally, the way shuffle is called looks like it will pose some issues.
You should modify your code so that the shuffle function selects a random element from the names array on its own and only call shuffle one time (you might also consider renaming shuffle to something like selectRandomName). Then change setTimeout to setInterval and only call that on mount (instead of on each render).
Here's a full example:
const names = [
'tony', 'elias', 'fadi'
]
function MyComponent() {
const [newName, setnewName] = useState("");
const shuffle = useCallback(() => {
const index = Math.floor(Math.random() * names.length);
setnewName(names[index]);
}, []);
useEffect(() => {
const intervalID = setInterval(shuffle, 5000);
return () => clearInterval(intervalID);
}, [shuffle])
return(
<Text>name:{newName}</Text>
)
}
Note the use of useCallback here is to prevent useEffect from running on each render while also preventing linter warnings from showing up.

How to map an array , and change the state asynchrony using react-hooks

I want a component to re-render every 5sec and displaying the array at the corresponding indexes, the array length is known to be exactly 10.
here is what I did so far :
const GameCard = ({ gameArray, startGame }) => {
const [arrayIndex, setArrayIndex] = useState(0);
let {questionWord} = gameArray[arrayIndex] ;
useEffect(() => {
if(!startGame) return;
let timer = setTimeout(() => {
if(arrayIndex === 9) return; //is this valid ?
setArrayIndex(arrayIndex +1)} , 1500)
return () => {
clearTimeout(timer)
}
}, [arrayIndex ,startGame]);
return (<div>{questionWord}</div>)
startGame is a boolean prop that gets changed on user click.
Now, this is working but as you can see im triggering the useEffect base on 2 variables, and when i reach the end of the array I'm returning inside setTimeout preventing the arrayIndex from updating.
This feels hacky, how can I improve my async useEffect?
and what happens when we return from setTimeout function , or useEffect?
It might be tempting to use functional updates like setArrayIndex((i) => i + 1):
If the new state is computed using the previous state, you can pass a function to setState.
However, your code needs to use the original value of the arrayIndex constant, i.e. if it was 0 by the time setTimeout was registered, you want it to be 0 even after 5 seconds.
This is the case already in your code - it will be different inside different renders (each timer will have different value because it was registered inside a different render), but the value will never change between registration and execution of a timer (see Closures).
As for improvements, it is possible to detect the arrayIndex === 9 even before registering a new setTimeout like this:
useEffect(() => {
if(!startGame || arrayIndex >= 9) return
const timer = setTimeout(() => {
setArrayIndex(arrayIndex + 1)
}, 5000)
return () => clearTimeout(timer)
}, [arrayIndex, startGame])
Moreover, if you want the timer to discount the render time (i.e. not 5 seconds AFTER each render, but 5 second intervals), you will need a mutable reference instead of an immutable state:
const arrayIndexRef = useRef(0)
const arrayIndex = arrayIndexRef.current
useEffect(() => {
if(!startGame) return
const timer = setInterval(() => {
arrayIndexRef.current += 1
if (arrayIndexRef.current >= 9) {
clearInterval(timer)
}
}, 5000)
return () => clearInterval(timer)
}, [startGame])

Resources