I am having trouble on changing count value in React.
I have dynamic set, and I want to render the size of set whenever it changes!
So I want to keep update count variable to ratedSet.size whenever the ratedSet's size changes.
I get it that useSet wont render again until whenever the thing ends so I tried to use useEffect and it still won't work. I think i am not using it correctly.
How can I show the size of set whenever it changes ?
Below are my Code. I added few comments on what i tried to do:
import React, { useEffect, useState } from "react";
import BoardCardMain from "./component/BoardCardSurvey";
//other import statements
export default function Survey() {
const [gameList, setGameList] = useState<Game[]>([]);
const [count, setCount] = useState(0); //This is the count variable that I want to keep update
const [width] = useState(window.innerWidth);
let ratedSet = new Set(); //this is the set that I want to keep track of
let cnt = 0;
useEffect(() => {
setGameList(tempData.gameList);
}, []);
// I tried to do this but im not even close to answer...
useEffect(() => {
setCount(ratedSet.size);
}, [ratedSet.size]);
//This is the part I am changing the size of set dynamically
//I am getting gameNo and score from child tsx, and am updating the set based on it
const countHandler = (ratedGameNo: number, score: number) => {
if (score > 0) {
ratedSet.add(ratedGameNo);
} else {
ratedSet.forEach((item) => {
if (item === ratedGameNo) {
ratedSet.delete(item);
}
});
}
//So here, if I console.log(ratedSet.size), it gives me correct values.
//but if i do setCount(ratedSet.size) it only updates once and never change again
};
return (
<>
SIZE IS : {count}
SIZE IS : {ratedSet.size}
None of them works
<Box> I am updating my set with below map : </Box>
<Container style={{ marginTop: 20, padding: 10 }}>
<Grid container spacing={2}>
{gameList.map((game) => (
<BoardCardMain
key={game.gameNo}
game={game}
parentCallback={countHandler}
></BoardCardMain>
))}
</Grid>
</Container>
</>
);
}
const tempData = {
gameList: [
{
gameNo: 1,
gameImg:
"https:/",
},
{
gameNo: 12,
gameImg:
"https://",
},
{
gameNo: 2,
gameImg:
"https://r",
},
],
};
Because ratedSet is not a state of the component. Mutating the set will NOT cause the component re-render. So the useEffect(() => {}, [ratedSet.size]) hook will not execute again.
I think there are two solutions:
Force updates the component when any changes are made to the ratedSet.
const forceUpdate: () => void = React.useState()[1].bind(null, {});
Maintain the ratedSet as a state, you can create a custom hook like useSet. (Recommend)
And you declare the ratedSet inside function component may be wrong. Because every time component rendering, you will create a new one.
I set rateSet as state as suggested and it works.
I removed Set and used array instead
const [count, setCount] = useState(0);
const [ratedGame, setRatedGame] = useState<number[]>([]);
const countHandler = (ratedGameNo: number, score: number) => {
if (score > 0) {
if (ratedGame.length === 0) {
ratedGame.push(ratedGameNo);
setCount(count + 1);
} else {
let found = ratedGame.includes(ratedGameNo) ? true : false;
if (found) {
} //do nothing
else {
setCount(count + 1);
ratedGame.push(ratedGameNo);
}
}
} else if (score === 0) {
setCount(count - 1);
var index = ratedGame.indexOf(ratedGameNo);
if (index !== -1) {
ratedGame.splice(index, 1);
}
}
};
Related
I'm building a function that makes images of random cars animate across the screen, and I want to stagger the population of the "carsLeft" array with a setTimeout..(which I will ultimately randomize the delay time).
everything works until I try to use a setTimeout. With the code below, no cars get are shown. a Console.log shows that the "carsLeft" array does not populate. When I remove the setTimeout all the cars are shown (at once of course). I have tried IIDE still no luck. Been stuck on this one for awhile, Please Help!
function Traffic() {
let carsLeft: any = [];
const generateCarLeft = () => {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
carsLeft.push(
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>
);
}, 3000);
}
};
generateCarLeft();
return <div className="traffic__container">{carsLeft}</div>;
}
export default Traffic;
If you want to generate elements through the loop and happen after the component is mounted is using React.useEffect hook, for example
React.useEffect(() => {
generateCarsLeft()
}, [])
and also using the carsLeft as a state will solve the problem.
If do without state and setTimeout it'll work because before first render(mount) all the data is available.
but if you use on first render list is empty and after setTimeout it'll update variable but react does not know that variable is updated you have to use the state to solve this issue.
function Traffic() {
const [carsLeft, setCarsLeft] = useState([]);
const generateCarLeft = () => {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
setCarsLeft((items) => [
...items,
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>,
]);
}, 3000);
}
};
generateCarLeft();
return <div className="traffic__container">{carsLeft}</div>;
}
A React component is essentially just a function, so if you declare a variable inside that function it will be re-created each time you call it. If you want any value to persist between calls it has to be saved somewhere else. In React that 'somewhere' is state.
Unless React detects changes to the state it won't even re-render the component.
So as of now your component is called once, 3 seconds later all 5 new elements are added to the array at once, but the component is no re-rendered, because React is not tracking this change. I'll give you an example of how you make it work, but I'd suggest you learn more about React's concepts of state and life cycle.
function Traffic() {
[carsLeft, setCarsLeft] = React.useState([]);
React.useEffect(()=>{
if(carsLeft.length > 4) return;
setTimeout(() => {
setCarsLeft( cl => [...cl,
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""/>]
);
}, 3000);
})
return <div className="traffic__container">{carsLeft}</div>;
}
This is what you are looking for
const list = ["Hello", "how", "are", "you"];
function App() {
const [myList, updateList] = useState([])
useEffect(() =>{
for (let i=0; i<list.length; i++) {
setTimeout(() => {
updateList((prvList) => ([...prvList, list[i]]));
}, 1000 * i) // * i is important
}
}, []);
return (
<div>
{
myList.map(li => (<div key={li}>{li}</div>))
}
</div>
);
}
Even if this did run, it would not run the way you want it to. Because the for loop is extremely fast, you would wait 3s to get 5 cars at once.
You need to get rid of the loop. And you need to wrap all side effects in a useEffect hook, and all persistent variables like carsLeft in a useState hook.
export default function Traffic() {
const [carsLeft, setCarsLeft] = useState<Array<ReactNode>>([]);
const timeout = useRef<number>();
useEffect(() => {
timeout.current = setTimeout(() => {
if (carsLeft.length < 5)
setCarsLeft([
...carsLeft,
<CarLeft key={carsLeft.length} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>
]);
else clearTimeout(timeout.current);
}, 3000);
return () => {
clearTimeout(timeout.current);
};
}, [carsLeft]);
return <div className="traffic__container">{carsLeft}</div>;
}
Codesandbox demo: https://codesandbox.io/s/naughty-driscoll-6k6u8?file=/src/App.tsx
I have solved all the issues and everything works perfectly. I used #Summer 's approach to remove the for loop and let the useEffect + State changes create its own endless loop. Instead of a setTimeout I used a setInterval method. An issue I had with Summer's solution was using "carsOnTheLeft.length" to generate key values. This caused multiple keys to have the same value and resulted with some issues. To fix this I created a new piece of state "carLeftIndex" to generate a unique key on every re-render. The generated objects are removed from the state array when the animation is complete thanks to React's "onAnimationEnd()' which triggers the "handleRemoveItem" function. Heres my working code, thank you for your answers!
const getRandomNumber = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};
const carListDay = [car_1, car_2, car_3, car_4, car_5, car_6];
function Traffic() {
const [carsOnTheLeft, setCarsOnTheLeft] = useState<any>([]);
const timeout = useRef<any>();
const [carLeftIndex, setCarLeftIndex] = useState(0);
useEffect(() => {
const getRandomNumberToString = (min: number, max: number) => {
const result = Math.random() * (max - min) + min;
return result.toString();
};
setCarLeftIndex(carLeftIndex + 1);
const CarLeft = styled.div`
animation-duration: ${getRandomNumberToString(3, 10)}s;
`;
timeout.current = setInterval(() => {
getRandomNumberToString(2, 9);
if (carsOnTheLeft.length < 15)
setCarsOnTheLeft([
...carsOnTheLeft,
<CarLeft
key={carLeftIndex}
className="car__left"
onAnimationEnd={(e) => handleRemoveItem(e)}
>
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>,
]);
else clearTimeout(timeout.current);
}, getRandomNumber(100, 5000));
console.log(carsOnTheLeft);
console.log(carLeftIndex);
const handleRemoveItem = (e: any) => {
setCarsOnTheLeft((carsOnTheLeft: any) =>
carsOnTheLeft.filter((key: any, i: any) => i !== 0)
);
};
return () => {
clearTimeout(timeout.current);
};
}, [carsOnTheLeft]);
return <div className="traffic__container">{carsOnTheLeft}</div>;
React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);
I am working of a Guessing Game for 'React Native' where the user enters a number and the phone tries to guess it. Each time the phone generates a guess the user can click Greater/Lower. When the user entered number and the computer made guess equal each other we are taken to the game over screen.
The game over screen is not rendering. The logic to render the game over screen is placed inside of a useEffect()
Problem
useEffect is only fired once during the mounting phase and never again?
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);*emphasized text*
(./screens/GameScreen.js)
We should exit the GameScreen when currentGuess === userSelectedNumber but this code is only run once.
Full code for GameScreen below:
import React, { useState, useRef, useEffect } from "react";
import { View, StyleSheet, Button, Text, Alert } from "react-native";
import NumberContainer from "../components/NumberContainer";
import Card from "../components/Card";
const randNumberGeneratorBetween = (min, max, exclude) => {
min = Math.ceil(min);
max = Math.floor(max);
const randNum = Math.floor(Math.random() * (max - min)) + min;
if (randNum === exclude) {
return randNumberGeneratorBetween(1, 100, exclude);
} else {
return randNum;
}
};
const GameScreen = props => {
const [currentGuess, setCurrentGuess] = useState(
randNumberGeneratorBetween(1, 100, props.userSelectedNumber)
);
const [rounds, setRounds] = useState(0);
const currentLow = useRef(1);
const currentHigh = useRef(100);
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);
const nextGuessHandler = direction => {
if (
(direction === "lower" && currentGuess < props.userSelectedNumber) ||
(direction === "greater" && currentGuess > props.userSelectedNumber)
) {
Alert.alert("Don't Lie", "You know this is wrong", [
{ text: "Sorry", style: "cancel" }
]);
}
if (direction === "lower") {
currentHigh.current = currentGuess;
} else {
currentLow.current = currentGuess;
}
const nextNumber = randNumberGeneratorBetween(
currentLow.current,
currentHigh.current,
currentGuess
);
console.log('nextNumber',nextNumber);
setCurrentGuess(nextNumber);
setRounds(currRounds => currRounds + 1);
console.log('currRound',rounds);
};
return (
<View style={styles.screen}>
<Text>Opponents Guess</Text>
<NumberContainer>{currentGuess}</NumberContainer>
<Card style={styles.buttonContainer}>
<Button
title="Lower"
onPress={nextGuessHandler.bind(this, "lower")}
></Button>
<Button
title="Greater"
onPress={nextGuessHandler.bind(this, "greater")}
></Button>
</Card>
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
padding: 10,
alignItems: "center"
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 20,
width: 300,
maxWidth: "80%"
}
});
export default GameScreen;
Project can be found here:
https://codesandbox.io/s/github/SMasood1/guessingGame?file=/screens/GameScreen.js:852-1039
You need to add rounds and currentGuess to the dependencies array in the useEffect hook
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver,currentGuess,rounds]);
Also it is considered a anti-pattern to use props to initialize a state, so I would recommend to add an other useEffect hook:
useEffect(()=>{
setCurrentGuess(randNumberGeneratorBetween(1, 100, props.userSelectedNumber))
},[props.userSelectedNumber]);
The useEffect hook causes the component to update whenever any of the values of the dependency array changes. Make sure the values you use to trigger that hook are in fact changing.
You can elegantly trigger useEffect by supplying a timestamp on you navigation.navigate call
e.g.
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal])
will only fire once, while
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true,
updateTs: new Date()
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(props.route.params?.showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal, props.route.params?.updateTs])
will fire every time you re-navigate to your screen via navigation.navigate()
I'm dynamically building React children based on an outside ordering. Unfortunately, the initial build process (represented below in the .forEach is very expensive:
const [amt, setAmt] = useState(0);
const [output, setOutput] = useState();
useEffect(() => {
const order = ["second", "first"];
// In reality, this is expensive logic to properly render the child components
order.forEach((item, index, array) => {
if (item === "first") {
array[index] = <First key={1} amt={amt} />;
} else {
array[index] = <Second key={2} amt={amt} />;
}
});
setOutput(order);
}, []); // does not work unless I pass the amt state, which I don't want :(
If I build the children once on load using useEffect(()=>{expensive operation},[]); the state doesn't get passed to the children.
If I build the order on each state change useEffect(()=>{expensive operation},[amt]); this works, but creates a lot of unnecessary overhead, rebuilding the children's order every time amt changes.
How can I dynamically include my First and Second components ONCE, but still give them access to the state vars I pass them?
Here is the full code:
import React, { useEffect, useState } from "react";
function First({ amt }) {
return <div id="first">{`First: ${amt}`}</div>;
}
function Second({ amt }) {
const dbl = amt * 2;
return <div id="second">{`Second ${dbl}`}</div>;
}
export default function Parent() {
const [amt, setAmt] = useState(0);
const [output, setOutput] = useState();
useEffect(() => {
const order = ["second", "first"];
// In reality, this is expensive logic to properly render the child components
order.forEach((item, index, array) => {
if (item === "first") {
array[index] = <First key={1} amt={amt} />;
} else {
array[index] = <Second key={2} amt={amt} />;
}
});
setOutput(order);
}, []); // does not work unless I pass the amt state, which I don't want :(
useEffect(() => {
const interval = setInterval(() => {
setAmt(amt => amt + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div>
Straight:
<First amt={amt} />
<Second amt={amt} />
<br />
Dynamic:
{output}
</div>
);
}
Also here: https://codesandbox.io/s/nifty-rain-hhjm4?file=/src/Parent.js
In short: I need to build my children out once, but make sure they still get the amt state every time it updates.
To restrict useEffect from running on the first render we can do:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
According to example here: https://stackoverflow.com/a/53351556/3102993
But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.
Some context:
const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");
useEffect(() => {
props.OnAmountChange(amount);
}, [amount]);
useEffect(() => {
props.OnTypeChange(type);
}, [type]);
return {
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
}
}
The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.
So if you are using this solution:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
useEffect (() => {
// second useEffect
if(!isFirstRun) {
console.log("Effect was run");
}
});
So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.
What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:
const {useState} = React
function useMounted() {
const [isMounted, setIsMounted] = useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
function App() {
const [valueFirst, setValueFirst] = useState(0)
const [valueSecond, setValueSecond] = useState(0)
const isMounted = useMounted()
//1st effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueFirst ran")
}
}, [valueFirst])
//2nd effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueSecond ran")
}
}, [valueSecond])
return ( <
div >
<
span > {
valueFirst
} < /span> <
button onClick = {
() => {
setValueFirst((c) => c + 1)
}
} >
Trigger valueFirstEffect < /button> <
span > {
valueSecond
} < /span> <
button onClick = {
() => {
setValueSecond((c) => c + 1)
}
} >
Trigger valueSecondEffect < /button>
<
/div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I hope it helps !!
You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.
Your original attempt:
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
The issue here is the if/elseif, treat these as independent effects instead:
useEffect(() => {
if (amount !== 0) props.onAmountChange(amount);
if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])
In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:
const Comp = () => {
const [ amount, setAmount ] = useState(null);
const [ type, setType ] = useState(null);
useEffect(() => {
if (amount !== null) {
props.onAmountChange(amount);
} else {
props.onAmountChange(0);
}
}, [amount]);
useEffect(() => {
if (type !== null) {
props.onTypeChange(type);
} else {
props.onTypeChange("Type1");
}
}, [type]);
return (
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
)
}
By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.
If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!
creds to #Dror Bar comment from react-hooks: skip first run in useEffect