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()
Related
import { useEffect } from 'react';
import { getXPosition, getYPosition } from 'helpers/location-helper';
import { useState } from 'react';
import { AircraftWrapper } from './aircraft.styles';
const Aircraft = ({ flightData, setFlight, flights }) => {
const flightId = flightData.flightId;
const [location, setLocation] = useState(flightData.coordinate);
const [currentState, setState] = useState(true);
useEffect(() => {
if(currentState === false) {
setFlight(flights.filter(item => item.flightId !== flightData?.flightId));
}
const interval = setInterval(() => {
setLocation({...location, x: (location.x+(10*Math.cos(Math.PI/180*flightData?.heading))), y: location.y+(10*Math.sin(Math.PI/180*flightData?.heading))});
if (location.x < -5 || location.y < 0 || location.x > 105 || location.y > 150) {
setState(false);
}
}, 1000);
return() => clearInterval(interval);
}, [location, flightData.heading]);
return(
<AircraftWrapper
data-testid="aircraft"
style={{left: `${getXPosition(location?.x)}`, top: `${getYPosition(location?.y)}`}}
heading={flightData?.heading+85}
/>
)
}
export default Aircraft;
//(150) + Math.cos(flightData.heading)*location.y
The error is in the if(currentState === false). It causes all aircrafts to be removed from the screen. What I am trying to do is based off of a timer, update the position of the aircraft and use a transition to move it forward until it goes past the map boundaries. as soon as it hits the map boundaries, it will disappear from the screen.
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);
}
}
};
I am hoping you guys can help me,
I think I may be missing a simple concept. I have a Component within that there is another component that has an array that is prop drilled from its parent component. In the Child component, the list is then mapped and displayed.
Now the issue is when I update the state of the Array, ie add another item to the Array using SetArray([...array, newItem]),
the useEffect in the ChildComponent will console.log the new array but the actual display does not change until I add another element to the array.
When I add another element the first element I added appears but the 2nd one doesn't.
Hopefully, that makes some sense
ChildComponent:
import React, { useState, useEffect } from "react";
////EDITOR//// List
import { Grid, Button, Card } from "#material-ui/core";
import Timestamp from "./Timestamp";
const TimestampList = ({ setTime, match, setMatchEdit, render }) => {
const [timestamps, setTimestamps] = useState([]);
useEffect(() => {
const setInit = async () => {
try {
console.log(match);
const m = await match.scores.map(player => {
console.log(player);
if (player.totalScores) {
return player.totalScores;
}
});
console.log(m);
if (m[0] && m[1]) {
setTimestamps(
[...m[0], ...m[1]].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[0] && !m[1]) {
setTimestamps(
m[0].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[1] && !m[0]) {
setTimestamps(
m[1].sort((a, b) => {
return a.time - b.time;
})
);
}
} catch (error) {
console.log(error);
}
};
if (match) {
setInit();
}
console.log(match);
}, [match]);
return (
<Grid
component={Card}
style={{ width: "100%", maxHeight: "360px", overflow: "scroll" }}
>
{timestamps && timestamps.map(timestamp => {
console.log(timestamp);
const min = Math.floor(timestamp.time / 60);
const sec = timestamp.time - min * 60;
const times = `${min}m ${sec}sec`;
return (
<Timestamp
time={times}
pointsScored={timestamp.points}
/>
);
})}
<Grid container direction='row'></Grid>
</Grid>
);
};
export default TimestampList;
Hi I am new developer at ReactJs. I have a problem about useEffect rendering. I have an example and i am trying to change background image with time but useEffect make rendering so much and my background image is staying in a loop with time. I just want to change my images with order like bg1 bg2 bg3 etc.
How can I solve this infinite render?
my example .tsx
import React, { useEffect, useState } from 'react';
import { connect } from "../../../store/store";
const LeftPart = (props: any) => {
const [value, setValue] = useState(1);
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};
useEffect(() => {
changeLeftBackgroungImage();
});
return (
<div className="col-xl-7 col-lg-7 col-md-7 col-sm col-12">
<img id="image" src={"../../../assets/images/bg"+value+".jpg"} style={{ width: "100%", height: "99vh" }} alt="Login Images"></img>
</div >
)
}
export default connect((store: any) => ({ loginLeftImagesLength: store.loginLeftImagesLength }))(LeftPart) as any;
You have infinite render as you have not specified any dependencies in your useEffect
useEffect(() => {
changeLeftBackgroungImage();
},[]); // Will run this hook on component mount.
Also you could do it in this way
useEffect(()=>{
const timer = setTimeout(()=>{
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
},3000)
return ()=>{ // Return this function from hook which is called before the hook runs next time
clearTimeout(timer)
}
},[,value]) // RUN THIS HOOK ON componendDidMount and whenever `value` changes
Why not put this entire code inside the useEffect hook.
useEffect(() => ) {
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};[changeBGStateHere])
use if else statements to change the background...
if (value === 1) {
changeLeftBackgroungImage();
} else (value === 2) {
and so on.
Let the interval change the state and let useEffect rerender when the state for the time changes.
i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.
import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { defProperty } from 'helpers';
const Tabs = ({ children, value, orientation, onChange }) => {
console.log(value);
const indicatorRef = useRef(null);
const tabsRef = useRef(null);
const childrenWrapperRef = useRef(null);
const valueToIndex = new Map();
const vertical = orientation === 'vertical';
const start = vertical ? 'top' : 'left';
const size = vertical ? 'height' : 'width';
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({});
const [transition, setTransition] = useState('none');
const getTabsMeta = useCallback(() => {
console.log('getTabsMeta');
const tabsNode = tabsRef.current;
let tabsMeta;
if (tabsNode) {
const rect = tabsNode.getBoundingClientRect();
tabsMeta = {
clientWidth: tabsNode.clientWidth,
scrollLeft: tabsNode.scrollLeft,
scrollTop: tabsNode.scrollTop,
scrollWidth: tabsNode.scrollWidth,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
}
let tabMeta;
if (tabsNode && value !== false) {
const wrapperChildren = childrenWrapperRef.current.children;
if (wrapperChildren.length > 0) {
const tab = wrapperChildren[valueToIndex.get(value)];
tabMeta = tab ? tab.getBoundingClientRect() : null;
}
}
return {
tabsMeta,
tabMeta,
};
}, [value, valueToIndex]);
const updateIndicatorState = useCallback(() => {
console.log('updateIndicatorState');
let _newIndicatorStyle;
const { tabsMeta, tabMeta } = getTabsMeta();
let startValue;
if (tabMeta && tabsMeta) {
if (vertical) {
startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
} else {
startValue = tabMeta.left - tabsMeta.left;
}
}
const newIndicatorStyle =
((_newIndicatorStyle = {}),
defProperty(_newIndicatorStyle, start, startValue),
defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
_newIndicatorStyle);
if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
setIndicatorStyle(newIndicatorStyle);
} else {
const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
if (dStart >= 1 || dSize >= 1) {
setIndicatorStyle(newIndicatorStyle);
if (transition === 'none') {
setTransition(`${[start]} 0.3s ease-in-out`);
}
}
}
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
useEffect(() => {
const timeout = setTimeout(() => {
setMounted(true);
}, 350);
return () => {
clearTimeout(timeout);
};
}, []);
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
let childIndex = 0;
const childrenItems = React.Children.map(children, child => {
const childValue = child.props.value === undefined ? childIndex : child.props.value;
valueToIndex.set(childValue, childIndex);
const selected = childValue === value;
childIndex += 1;
return React.cloneElement(child, {
selected,
indicator: selected && !mounted,
value: childValue,
onChange,
});
});
const styles = {
[size]: `${indicatorStyle[size]}px`,
[start]: `${indicatorStyle[start]}px`,
transition,
};
console.log(styles);
return (
<>
{value !== 2 ? (
<div className={`tabs tabs--${orientation}`} ref={tabsRef}>
<span className="tab__indicator-wrapper">
<span className="tab__indicator" ref={indicatorRef} style={styles} />
</span>
<div className="tabs__wrapper" ref={childrenWrapperRef}>
{childrenItems}
</div>
</div>
) : null}
</>
);
};
Tabs.defaultProps = {
orientation: 'horizontal',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
onChange: PropTypes.func.isRequired,
};
export default Tabs;
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
This effect will trigger whenever the value of mounted or updateIndicatorState changes.
const updateIndicatorState = useCallback(() => {
...
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
The value of updateIndicatorState will change if any of the values in its dep array change, namely getTabsMeta.
const getTabsMeta = useCallback(() => {
...
}, [value, valueToIndex]);
The value of getTabsMeta will change whenever value or valueToIndex changes. From what I'm gathering from your code, value is the value of the selected tab, and valueToIndex is a Map that is re-defined on every single render of this component. So I would expect the value of getTabsMeta to be redefined on every render as well, which will result in the useEffect containing updateIndicatorState to run on every render.