Reputation: 37
The app I'm working on is a multi-game app (a bird game, mole game, and minesweeper). The start screen is the menu. It gives me a cycle warning on this screen. When I tap to start the mole game, it plays naturally, but the moment I press the back button to go back to the menu, I receive errors: "Can't perform a React state update on an unmounted component" error follow by continuous "TYPE ERORR: null is not an object".
import {
View,
StyleSheet,
Image,
SafeAreaView,
Text,
TouchableWithoutFeedback
} from 'react-native';
import Images from './assets/Images';
import Constants from './Constants';
import Mole from './Mole';
import GameOver from './GameOver';
import Clear from './Clear';
import Pause from './Pause';
const DEFAULT_TIME = 20;
const DEFAULT_STATE = {
level: 1,
score: 0,
time: DEFAULT_TIME,
cleared: false,
paused: false,
gameover: false,
health: 100
}
export default class MoleGame extends Component {
constructor(props) {
super(props);
this.moles = [];
this.state = DEFAULT_STATE;
this.molesPopping = 0;
this.interval = null;
this.timeInterval = null;
}
componentDidMount = () => {
this.setupTicks(DEFAULT_STATE, this.pause);
}
setupTicks = () => {
let speed = 750 - (this.state.level * 50);
if (speed < 350) {
speed = 350;
}
this.interval = setInterval(this.popRandomMole, speed);
this.timeInterval = setInterval(this.timerTick, 1000);
}
reset = () => {
this.molesPopping = 0;
this.setState(DEFAULT_STATE, this.setupTicks)
}
pause = () => {
if (this.interval) clearInterval(this.interval);
if (this.timeInterval) clearInterval(this.timeInterval);
this.setState({
paused: true
});
}
resume = () => {
this.molesPopping = 0;
this.setState({
paused: false
}, this.setupTicks);
}
nextLevel = () => {
this.molesPopping = 0;
this.setState({
level: this.state.level + 1,
cleared: false,
gameover: false,
time: DEFAULT_TIME
}, this.setupTicks)
}
timerTick = () => {
if (this.state.time === 0) {
clearInterval(this.interval);
clearInterval(this.timeInterval);
this.setState({
cleared: true
})
} else {
this.setState({
time: this.state.time - 1
})
}
}
gameOver = () => {
clearInterval(this.interval);
clearInterval(this.timerInterval);
this.setState({
gameover: true
})
}
onDamage = () => {
if (this.state.cleared || this.state.gameOver || this.state.paused) {
return;
}
let targetHealth = this.state.health - 10 < 0 ? 0 : this.state.health - 20;
this.setState({
health: targetHealth
});
if (targetHealth <= 0) {
this.gameOver();
}
}
onHeal = () => {
let targetHealth = this.state.health + 10 > 100 ? 100 : this.state.health + 10
this.setState({
health: targetHealth
})
}
onScore = () => {
this.setState({
score: this.state.score + 1
})
}
randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
onFinishPopping = (index) => {
this.molesPopping -= 1;
}
popRandomMole = () => {
if (this.moles.length != 12) {
return;
}
let randomIndex = this.randomBetween(0, 11);
if (!this.moles[randomIndex].isPopping && this.molesPopping < 3) {
this.molesPopping += 1;
this.moles[randomIndex].pop();
}
}
render() {
let healthBarWidth = (Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60 - Constants.XR * 6) * this.state.health / 100;
return (
<View style={styles.container}>
<Image style={styles.backgroundImage} resizeMode="stretch" source={Images.background} />
<View style={styles.topPanel}>
<SafeAreaView>
<View style={styles.statsContainer}>
<View style={styles.stats}>
<View style={styles.levelContainer}>
<Text style={styles.levelTitle}>Level</Text>
<Text style={styles.levelNumber}>{this.state.level}</Text>
</View>
</View>
<View style={styles.stats}>
<View style={styles.timeBar}>
<Text style={styles.timeNumber}>{this.state.time}</Text>
</View>
<Image style={styles.timeIcon} resizeMode="stretch" source={Images.timeIcon} />
</View>
<View style={styles.stats}>
<View style={styles.scoreBar}>
<Text style={styles.scoreNumber}>{this.state.score}</Text>
</View>
<Image style={styles.scoreIcon} resizeMode="stretch" source={Images.scoreIcon} />
</View>
<View style={styles.stats}>
<TouchableWithoutFeedback onPress={this.pause}>
<View style={styles.pauseButton}>
<Image style={styles.pauseButtonIcon} resizeMode="stretch" source={Images.pauseIcon} />
</View>
</TouchableWithoutFeedback>
</View>
</View>
<View style={styles.healthBarContainer}>
<View style={styles.healthBar}>
<View style={[styles.healthBarInner, { width: healthBarWidth }]} />
</View>
<Image style={styles.healthIcon} resizeMode="stretch" source={Images.healthIcon} />
</View>
</SafeAreaView>
</View>
<View style={styles.playArea}>
{Array.apply(null, Array(4)).map((el, rowIdx) => {
return (
<View style={styles.playRow} key={rowIdx}>
{Array.apply(null, Array(3)).map((el, colIdx) => {
let moleIdx = (rowIdx * 3) + colIdx;
return (
<View style={styles.playCell} key={colIdx}>
<Mole
index={moleIdx}
onDamage={this.onDamage}
onHeal={this.onHeal}
onFinishPopping={this.onFinishPopping}
onScore={this.onScore}
ref={(ref) => { this.moles[moleIdx] = ref }}
/>
</View>
)
})}
</View>
)
})}
</View>
{this.state.cleared && <Clear onReset={this.reset} onNextLevel={this.nextLevel} level={this.state.level} score={this.state.score} />}
{this.state.gameover && <GameOver onReset={this.reset} level={this.state.level} score={this.state.score} />}
{this.state.paused && <Pause onReset={this.reset} onResume={this.resume} />}
</View>
)
}
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column'
},
backgroundImage: {
width: Constants.MAX_WIDTH,
height: Constants.MAX_HEIGHT,
position: 'absolute'
},
topPanel: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: Constants.YR * 250,
flexDirection: 'column'
},
statsContainer: {
width: Constants.MAX_WIDTH,
height: Constants.YR * 120,
flexDirection: 'row'
},
stats: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
pauseButton: {
width: Constants.YR * 50,
height: Constants.YR * 50,
backgroundColor: 'black',
borderColor: 'white',
borderWidth: 3,
borderRadius: 10,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
pauseButtonIcon: {
width: Constants.YR * 25,
height: Constants.YR * 25,
},
levelContainer: {
width: Constants.YR * 80,
height: Constants.YR * 80,
backgroundColor: '#ff1a1a',
borderColor: 'white',
borderWidth: 3,
borderRadius: 10,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
levelTitle: {
fontSize: 21,
color: 'white',
fontFamily: 'LilitaOne'
},
levelNumber: {
fontSize: 17,
color: 'white',
fontFamily: 'LilitaOne'
},
scoreIcon: {
position: 'absolute',
left: 0,
width: Constants.YR * 40,
height: Constants.YR * 40,
},
scoreBar: {
height: Constants.YR * 25,
position: 'absolute',
left: 20,
right: 5,
backgroundColor: 'white',
borderRadius: 13,
justifyContent: 'center',
alignItems: 'center'
},
scoreNumber: {
fontSize: 17,
color: 'black',
fontFamily: 'LilitaOne',
},
timeIcon: {
position: 'absolute',
left: 0,
width: Constants.YR * 40,
height: Constants.YR * 40,
},
timeBar: {
height: Constants.YR * 25,
position: 'absolute',
left: 20,
right: 5,
backgroundColor: 'white',
borderRadius: 13,
justifyContent: 'center',
alignItems: 'center'
},
timeNumber: {
fontSize: 17,
color: 'black',
fontFamily: 'LilitaOne',
},
healthBarContainer: {
height: Constants.YR * 40,
width: Constants.MAX_WIDTH - Constants.XR * 120,
marginLeft: Constants.XR * 60
},
healthIcon: {
position: 'absolute',
top: 0,
left: 0,
width: Constants.YR * 46,
height: Constants.YR * 40,
},
healthBar: {
height: Constants.YR * 20,
width: Constants.MAX_WIDTH - Constants.XR * 100 - Constants.XR * 60,
marginLeft: Constants.XR * 40,
marginTop: Constants.YR * 10,
backgroundColor: 'white',
borderRadius: Constants.YR * 10
},
healthBarInner: {
position: 'absolute',
backgroundColor: '#ff1a1a',
left: Constants.XR * 3,
top: Constants.YR * 3,
bottom: Constants.YR * 3,
borderRadius: Constants.YR * 8
},
playArea: {
width: Constants.MAX_WIDTH,
marginTop: Constants.YR * 250,
height: Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112,
flexDirection: 'column',
},
playRow: {
height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
width: Constants.MAX_WIDTH,
flexDirection: 'row',
},
playCell: {
width: Constants.MAX_WIDTH / 3,
height: (Constants.MAX_HEIGHT - Constants.YR * 250 - Constants.YR * 112) / 4,
alignItems: 'center'
}
});
import { View, StyleSheet, Button, Image, TouchableWithoutFeedback } from 'react-native';
import Images from './assets/Images';
import SpriteSheet from 'rn-sprite-sheet';
//import Constants from './Constants';
export default class Mole extends Component {
constructor (props) {
super(props);
this.mole = null;
this.actionTimeout = null;
this.isPopping = false;
this.isWacked = false;
this.isHealing = false;
this.isAttacking = false;
this.isFeisty = false;
}
pop =()=>{
this.isWacked = false;
this.isPopping = true;
this.isAttacking = false;
this.isFeisty = Math.random() < 0.4;
if(!this.isFeisty){
this.isHealing = Math.random() < 0.12;
}
if(this.isHealing){
this.mole.play({
type: "heal",
onFinish : ()=>{
this.actionTimeout = setTimeout(()=>{
this.mole.play({
type : "hide",
fps: 24,
onFinish: ()=>{
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000);
}
})
}
else{
this.mole.play({
type : "appear",
fps: 24,
onFinish: ()=>{
if (this.isFeisty){
this.actionTimeout = setTimeout(() => {
this.isAttacking = true;
this.props.onDamage();
this.mole.play({
type: "attack",
fps: 12,
onFinish: () => {
this.mole.play({
type: "hide",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}, 1000)
}
else{
this.actionTimeout = setTimeout(()=>{
this.mole.play({
type : "hide",
fps: 24,
onFinish: ()=>{
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}, 1000);
}
}
})
}
}
whack = ()=>{
if(!this.isPopping || this.isWacked || this.isAttacking){
return;
}
if (this.actionTimeout){
clearTimeout(this.actionTimeout);
}
this.isWacked = true;
this.props.onScore ();
if( this.isHealing){
this.props.onHeal();
}
this.mole.play({
type: "dizzy",
fps: 24,
onFinish: () => {
this.mole.play({
type: "faint",
fps: 24,
onFinish: () => {
this.isPopping = false;
this.props.onFinishPopping(this.props.index);
}
})
}
})
}
render() {
return (
<View style= {styles.container}>
<SpriteSheet
ref= {ref=> {this.mole = ref}}
source = {Images.sprites}
columns = {6}
rows = {8}
width = {100}
animations = {{
idle: [0],
appear: [1,2,3,4],
hide: [4,3,2,1,0],
dizzy : [36,37,38],
faint: [42,43,44,0],
attack: [11,12,13,14,15,16],
heal: [24,25,26,27,28,29,30,31,32,33]
}} />
<TouchableWithoutFeedback onPress= {this.whack} style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }}>
<View style= {{position: 'absolute', top: 0 , bottom:0, left:0, right:0 }} />
</TouchableWithoutFeedback>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1
}
})
import React from "react";
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import MoleGame from './Game1/App.js'
import BirdGame from './Game2/App.js'
import HomeScreen from './Game3/screens/HomeScreen.js'
import GameScreen from './Game3/screens/GameScreen.js'
import Home from './Home.js'
const Stack = createStackNavigator();
export default App = () => {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false, gestureEnabled: false, }} >
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="MoleGame" component={MoleGame} />
<Stack.Screen name="BirdGame" component={BirdGame} />
<Stack.Screen name="MineSweeperHome" component={HomeScreen} />
<Stack.Screen name="MineSweeperGame" component={GameScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
import { View, Text, TouchableOpacity, Dimensions, StyleSheet } from 'react-native';
const GameButton = ({ label, onPress, style }) => {
return (
<TouchableOpacity
style={style}
onPress={onPress}
>
<Text style={styles.text}>{label}</Text>
</TouchableOpacity>
);
};
export default Home = ({ navigation }) => {
return (
<View style={styles.container}>
<View style={styles.welcome}>
<Text style={styles.text}> Welcome! </Text>
</View>
<View style={styles.buttonContainer}>
<GameButton
label={'Play Bird Game'}
onPress={() => navigation.navigate('BirdGame')}
style={styles.gameButton}
/>
<GameButton
label={'Play Mole Game'}
onPress={() => navigation.navigate('MoleGame')}
style={{ ...styles.gameButton, backgroundColor: 'green' }}
/>
<GameButton
label={'Play MineSweeper Game'}
onPress={() => navigation.navigate('MineSweeperHome')}
style={{ ...styles.gameButton, backgroundColor: 'grey' }}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFCB43'
},
welcome: {
alignItems: 'center',
justifyContent: 'center',
padding: 10,
marginTop: 80,
},
buttonContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly'
},
gameButton: {
height: 70,
width: Dimensions.get("screen").width / 1.4,
backgroundColor: 'red',
borderColor: 'red',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 20,
borderWidth: 2,
borderColor: 'black',
},
text: {
fontSize: 22,
}
})
After adding
componentWillUnmount(){
clearInterval(this.interval);
clearInterval(this.timerInterval);
}
to the App.js. I no longer get continuous "TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')". Unfortunately, I still get TypeError: null is not an object (evaluating '_this.moles.play) error. This happens when I go back to the main menu. I've tried adding clearTimeout(this.actionTimeout)
on mole.js, but that didn't give any effect.
TypeError: null is not an object (evaluating '_this.moles.play)
Upvotes: 1
Views: 136
Reputation: 37
componentWillUnmount(){
clearInterval(this.interval);
clearInterval(this.timerInterval);
}
it fixed the continuous TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')
this.mole.play
into a variable within Mole.js because this
no longer refer to this.mole.play
when I go back to the menu. There is still memory leak warnings since it is finishing the animation as I go to the menu, but there are no more critical errors.Upvotes: 0
Reputation: 20830
The
setInterval()
function is commonly used to set a delay for functions that are executed again and again, such as animations. You can cancel the interval usingclearInterval()
.
When you navigate from MoleGame
to Home
, the MoleGame
route is popped off the navigation stack and its component is unmounted. But the intervals from the setupTicks
method are still executing, and trying to set state on the MoleGame
component and to access this
(neither of which are possible).
Try clearInterval
on componentWillUnmount
to stop the intervals that are set in the setupTicks
method.
// App.js
export default class MoleGame extends Component {
constructor(props) {
super(props);
this.interval = null;
this.timeInterval = null;
...
}
componentDidMount = () => {
this.setupTicks(DEFAULT_STATE, this.pause);
}
componentWillUnmount() {
clearInterval(this.interval);
clearInterval(this.timeInterval);
}
}
Similarly in Mole.js
you would have to handle the same scenario for any setTimeout()
s you have by using clearTimeout()
Further reading: Plugging memory leaks in your app
Upvotes: 1