"Can't perform a React state update on an unmounted component" Follow by continuous "TYPE ERORR: null is not an object"

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".

Screenshots:

Cycle warning

Null is not an object

Node.js errors

Mole Game's App.js

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'
    }
});

Mole.js

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
    }
})

index.js (Navigation)

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>
    );
};

Home.js

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,
    }
})

Some context and notices

UPDATE :

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.

Screenshot :

TypeError: null is not an object (evaluating '_this.moles.play)

Upvotes: 1

Views: 136

Answers (2)

  1. By adding:
componentWillUnmount(){
        clearInterval(this.interval);
        clearInterval(this.timerInterval);
    }

it fixed the continuous TypeError: null is not an object (evaluating '_this.moles[randomIndex].isPopping')

  1. To remove the "TypeError: null is not an object (evaluating '_this.moles.play)" error, I had to put 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

ksav
ksav

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 using clearInterval().

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

Related Questions